1.概念
Node.js 就是运行在服务端的 JavaScript,起初段定位是后端开发语言,由于技术的不够成熟,一般小型项目会完全使用node.js作为后台支撑,大项目中,运行不够稳定,不会轻易使用。具有高并发优良特性,Node.js是一个事件驱动I/O服务端JavaScript环境,基于Google的V8引擎,V8引擎执行Javascript的速度非常快,性能非常好.Node.js是事件驱动,非阻塞式I/O模型,Node.js使用npm包管理器。
使用Node.js不仅实现了一个应用,也实现了整个HTTP服务器,所以不需要单独搭建Apache或nginx。
2.主要模块:
1.fs 文件操作
var fs=require(“fs”)
a,创建文件,并且写入数据
fs.writeFile("demo.txt","hello,world!",function(err,data){
if(err)console.log(err)
console.log("create file success")
})
b重命名文件或者移动文件
fs.rename("demo.txt","demo1.txt",function(err){
//把demo.txt重命名为demo1.txt
if(err)console.log(err);
console.log("rename success")
})
fs.rename("demo.txt","demo/demo.txt",function(err){
//把demo.txt移动到demo文件夹下,
if(err)console.log(err);
console.log("rename success")
})
c删除文件
fs.unlink("text.txt", function(err) {
if (err) throw err;
console.log('successfully deleted');
});
D读取文件
fs.readFile('demo.txt','utf-8',function(err,data){
//使用utf-8的编码异步读取文件
if(err){
console.log(data);
}else{
console.log(data);
}
})
//使用utf-8的方式同步读取某个文件
var data = fs.readFileSync('demo.txt','utf-8');
console.log(data);
2.http ,http服务 ,创建服务器
response 响应
request 请求
3.global 全局对象(类似于window对象)
4.module(commonJS模块开发工具)
3.运行机制
- V8引擎解析JavaScript脚本。
- 解析后的代码,调用Node API。
- libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。
- V8引擎再将结果返回给用户。
4.node.js中的javascript
Node.js 中 Javascript 的组成部分
ECMAScript 核心 + 全局成员 + 核心 API 模块
全局成员:console、setInterval、setTimeout。。。
核心 API 模块:就是 Node 平台 单独提供的一些API,这些API是Node平台所独有的;
注意:Node.js 中 没有 BOM 和 DOM
由于 Node 服务器端运行环境中,没有浏览器 和 HTML 的概念,所以,Node中的javascript 提出了 DOM 和 BOM 这两个对象模型,取而代之的,是 全局成员 和 核心 API 模块;
5.常用命令
npm init 然后后面直接按照提示按 y / n , 注意: -y 表示一切默认
安装项目依赖包:
npm install express
注意: --save 将安装记录写入到package.json的项目依赖中
卸载依赖包:
npm uninstall express
更新依赖包:
npm update express
搜索依赖包:
npm search express
安装全局淘宝镜像
1npm install -g cnpm --registry=https://registry.npm.taobao.org
//退出环境
ctrl + c
6.主要的全局对象
与浏览器中window作为全局不同,在node中global是全局对象,我们可以直接在全局对象上定义属性,那么就可以访问到了,指的注意的是,由于每一个nodejs模块都是一个作用域,所以直接var是局部变量,而不是全局变量。
__filename — 这个全局变量表示运行的nodejs的文件名。
__dirname — 表示目录名称(不包含文件名)
其中__filename是包含了路径的。 而__dirname仅仅是缺少了文件名,只有路径。
console、setTimeout、clearInterval等等都是全局对象,
可以看到,由于setInterval和setTimeout是非阻塞的,所以后面的语句先执行,指的注意的是其中的console.log("%d",19950628)的应用,这与C语言中的printf是非常相似的。另外console.trace()可以追踪调用栈。
另外还有一个比较重要的api,即setImmediate(handler); 他是IE10中支持的。其他的浏览器一律不支持,但是node是支持的,这个解决单线程阻塞的问题,当然用setTimeout(handler, 0);也可以,但是后者的延迟时间较前者更长一些。
process也是一个全局变量,在node环境下输入global.process就可以看到其中具有的变量,因为process本身就是一个对象。不难理解process是描述进程(process即进程的意思)的一个全局对象。
他还有一些事件,如下:
process.on(“exit”, function (code) {
console.log(“exitCode is:”, code);
});
即进程一旦结束,就会触发监听器。 注意:这里使用on的方式,所以我们可以认为process是eventEmitter的实例。
退出码为0是什么意思呢? 因为每当exit事件触发,都会有一个code即退出码,表示这个退出的方式,0表示正常退出,一般还有如下几种退出码:
process 不仅提供了上述事件,还提供了非常多的有关进程的属性,如pid(进程号)、platform(程序运行的平台)、archf(当前CPU的架构)、title(进程名,默认为node)、versions(包含了node的版本和依赖)、version(node的版本)、execPath(当前脚本的二进制文件路径)、stdin、stdout、stderr。
:其中的stdout.write是在终端输出,那么什么时候才能在页面上输出呢? 显然,由于node是服务器端语言,所以说只能通过响应(response)的方式才能返回给客户端。
console.log(process.memoryUsage());
console.log(process.cwd());cwd(current working directory)即当前工作目录。
rss、heapTotal、heapUsed表示了内存使用情况。
7. node中的npm:
安装node时就已经安装了npm,即一个包管理工具,我们一般利用它来安装一些包,即npm install 如果后面添加 -g ,那么就会将包安全到全局环境,即user路径下。如果不添加,就会安装在当前文件夹下,当然首先会创建node_modules文件,在此之下。
一般,我们要创建一个项目时,我们可以先npm init,通过它,我们就可以创建一个package.json文件,然后通过配置该文件说明我们的项目信息。其中主要的参数有:
(1)name — 包名
(2)version — 版本号
(3) description — 包的描述
(4) homepage — 包的官网url
(5) author — 包的作者名字
(6) contributors — 包的其他贡献者名字
(7)dependencies — 依赖包列表(如果依赖包没有安装,npm 会自动将依赖包安装在 node_module 目录下)
(8)repository — 包代码存放的地方
(9)main — main 字段是一个模块ID,它是一个指向你程序的主要项目。就是说,如果你包的名字叫 express,然后用户安装它,然后require(“express”)
(10) keywords — 关键字
8.node.js的优缺点
• 事件驱动,通过闭包很容易实现客户端的生命活期。
• 不用担心多线程,锁,并行计算的问题
• V8引擎速度非常快
• 对于游戏来说,写一遍游戏逻辑代码,前端后端通用
当然Nodejs也有一些缺点:
• nodejs更新很快,可能会出现版本兼容
• nodejs还不算成熟,还没有大制作
• nodejs不像其他的服务器,对于不同的链接,不支持进程和线程操作
9.odejs事件循环
nodejs事件循环利用的是观察者模式,也就是发布订阅模式。简单的理解,DOM元素绑定事件就是这样的模式。其中绑定的元素是发布者,函数是订阅者,当元素发生了变化时(被点击等),就会通知所有的订阅者。
nodejs使用事件驱动模型,当服务器接受到了请求之后,就会关闭这个请求,然后再处理,为的是等待下一个请求。这样,请求就不会被耽搁。这个模型的效率非常高,因为他一直在接受请求,而没有等待任何读写操作。在事件驱动模型中,会生成一个主循环来监听事件,当检测到事件时触发回调函数:
虽然这里没有所谓的DOM元素,但是实现应当是一样的,即观察者模式的最好理解是好莱坞电影中的一句话: 你不要打电话给我,我会打电话给你。
在node中我们常常使用events模块来实现,即首先引入events,然后创建一个对象,利用这个对象的on方法绑定时间,利用对象的emit方法来触发事件,如下所示:
varevents = require("events");vareventEmitter =new events.EventEmitter();
eventEmitter.on("selfDefine", function () {
console.log("This is selfDefine1");
});
eventEmitter.on("selfDefine", function () {
console.log("This is selfDefine2");
});
eventEmitter.on("selfDefine", function () {
console.log("This is selfDefine3");
});
eventEmitter.on("selfDefine", function () {
console.log("This is selfDefine4");
});
eventEmitter.emit("selfDefine");
console.log("over");
在这里,我们就可以认为这是发布订阅者模式,首先可以知道发布者是selfDefine事件,订阅者是4个,一旦selfDefine被触发,那么就会通知订阅者,方法是将订阅者添加到了Event Loop中去,然后通过事件循环来监听,一旦被触发,就会通知,即我给你打电话,你没有给我打电话。
当事件触发时,注册到这个事件的事件监听器被依次调用。
另外,在on和emit中是可以传递参数的,如下所示:
varevents = require("events");vareventEmitter =new events.EventEmitter();
eventEmitter.on("selfDefine", function (x) {
console.log("This is selfDefine1 "+ x);
});
eventEmitter.on("selfDefine", function (x) {
console.log("This is selfDefine2 "+ x);
});
eventEmitter.on("selfDefine", function (x) {
console.log("This is selfDefine3 "+ x);
});
eventEmitter.on("selfDefine", function (x) {
console.log("This is selfDefine4 "+ x);
});
eventEmitter.emit("selfDefine","argument");
console.log("over");
关于 EventEmitter 还有其他的属性,如下所示:
addListener(event, listener) — 它和on是类似的,都是添加某一个事件的监听器。
removeListener(event, listener) — 即通过此API可以将监听器取消(特定的listener)。
removeAllListeners(event) — 可以取消event下的所有监听器。
newListener(event, listener); — 该事件在添加新的监听器时被触发。
listenerCount(emitter, event); — 返回指定监听器的数量。
listeners(event) — 返回指定事件的监听器数组。
once(event, listener) — 通过once就可以知道,这个监听器只会监听一次,后面再调用,就不会监听了。
举例如下:
varevents = require("events");vareventEmitter =new events.EventEmitter();
eventEmitter.on("foo", function () {
console.log("via on");
});
eventEmitter.once("foo", function () {
console.log("via once");
});
eventEmitter.emit("foo");
setTimeout(function () {
eventEmitter.emit("foo");
}, 1000);
在执行过程中vai on和via once是同时出现的,过了1s之后,via on 出现, via once不再出现,因为通过once添加的监听器只会监听一次,然后就被销毁了(即后面不再监听)。
10.Node模块机制
Node中,每个文件模块都是一个对象,它的定义如下:
function Module(id, parent) {
this.id = id;
this.exports = {};
this.parent = parent;
this.filename = null;
this.loaded = false;
this.children = [];
}
module.exports = Module;
var module = new Module(filename, parent);
所有的模块都是 Module 的实例。可以看到,当前模块(module.js)也是 Module 的一个实例。
11. require的模块加载机制
1、先计算模块路径
2、如果模块在缓存里面,取出缓存
3、加载模块
4、的输出模块的exports属性即可
// require 其实内部调用 Module._load 方法
Module._load = function(request, parent, isMain) {
// 计算绝对路径
var filename = Module._resolveFilename(request, parent);
// 第一步:如果有缓存,取出缓存
var cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports;
// 第二步:是否为内置模块
if (NativeModule.exists(filename)) {
return NativeModule.require(filename);
}
/********************************这里注意了**************************/
// 第三步:生成模块实例,存入缓存
// 这里的Module就是我们上面的1.1定义的Module
var module = new Module(filename, parent);
Module._cache[filename] = module;
/********************************这里注意了**************************/
// 第四步:加载模块
// 下面的module.load实际上是Module原型上有一个方法叫Module.prototype.load
try {
module.load(filename);
hadException = false;
} finally {
if (hadException) {
delete Module._cache[filename];
}
}
// 第五步:输出模块的exports属性
return module.exports;
};
12.任务队列?
12.1任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,"任务队列"上第一位的事件就自动进入主线程。
12.1 同步和异步任务#
- 同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
- 异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
12.2 执行流程# - 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
- 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
- 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
- 主线程不断重复上面的第三步。
13.express或koa框架的基本架构
区别
一、框架
- Express 是一个Node.js的基础框架,主要基于 Connect 中间件,并且自身封装了路由(需要配合bodyParser)、视图处理等功能,使用人数众多,弊端是callback回调方式。
- Koa 是一个比Express更精简,使用node新特性的中间件框架。其提供的是一个架子,而几乎所有的功能都需要由第三方中间件完成,比如koa-router, koa-view等。
[!NOTE]
Koa 利用 co 作为底层运行框架,利用 Generator 的特性,实现“无回调”的异步处理
二、处理路由#
Express使用 express.Router 类来创建可安装的模块化路由处理程序。Router 实例是完整的中间件和路由系统,以下示例将路由器创建为模块,在其中装入中间件,定义一些路由,然后安装在主应用程序的路径中。
var express = require('express');
var router = express.Router();
router.use(function timeLog(req, res, next) {
console.log('Time: ', Date.now());
next();
});
// define the home page route
router.get('/', function(req, res) {
res.send('Birds home page');
});
// define the about route
router.get('/about', function(req, res) {
res.send('About birds');
});
module.exports = router;
接着,在应用程序中装入路由器模块:
var routes = require('./route');
...
app.use('/route', routes);
Koa路由处理Koa 需要引入中间件
var koa = require('koa')
var route = require('koa-route') //中间件
var app = koa()
app.use(route.get('/', function *(){
this.body = 'Hello World'
}))
三、 HTTP Request
两个框架都封装了HTTP Request对象,有一点不同是 Koa v1 使用 this 取代 Express 的 req、res。
Express:
var app = require('express')()
app.get('/room/:id', function (req, res) {
console.log(req.params)
})
// 获取POST数据需要 body-parser 中间件
var bodyParser = require('body-parser')
app.use(bodyParser.json())
app.post('/sendgift', function (req, res) {
console.log(req.body)
})
Koa:
var app = require('koa')()
var route = require('koa-route')
app.use(route.get('/room/:id', function *() {
console.log(this.req.query)
}))
// 获取POST数据需要 co-body 中间件
var parse = require('co-body')
app.use(route.post('/sendgift', function *() {
var post = yield parse(this.request)
console.log(post)
}))
三 异步流程控制#
Express 采用 callback 来处理异步,
Koa v1 采用 generator,
Koa v2 采用 async/await。
四、错误处理
Express 使用 callback 捕获异常,对于深层次的异常捕获不了,
Koa 使用 try catch,能更好地解决异常捕获。
// Express callback
app.use(function (err, req, res, next) {
console.error(err.stack)
res.status(500).send('Something broke!')
})
// Koa generator
app.use(function *(next) {
try {
yield next
} catch (err) {
this.status = err.status || 500
this.body = { message: err.message }
this.app.emit('error', err, this)
}
})
// Koa async/await
app.use(async (ctx, next) => {
try {
await next()
} catch (err) {
ctx.status = err.status || 500
ctx.body = { message: err.message }
ctx.app.emit('error', err, this)
}
})
五、中间件处理
- Express中app.use就是往中间件数组中塞入新的中间件,中间件处理方式是线性的,next过后继续寻找下一个中间件。 一个请求进来经过一系列中间件处理后再响应给用户,清晰明了。
- 缺点:基于 callback 组合业务逻辑,业务逻辑复杂时嵌套过多,异常捕获困难。
- Koa的中间件处理方式是一个洋葱模型,koa处理完中间件后还会回来走一趟,这就给了我们更加大的操作空间。
const Koa = require(‘koa’);
const app = new Koa();
// x-response-time
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set(‘X-Response-Time’, ${ms}ms
);
});
// logger
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(${ctx.method} ${ctx.url} - ${ms}
);
});
// response
app.use(async ctx => {
ctx.body = ‘Hello World’;
14. Node创建线程的方法和区别
一、Node的单线程:
- Node.js 是以单线程的模式运行的,但它使用的是事件驱动来处理并发,这样有助于我们在多核 cpu 的系统上创建多个子进程,从而提高性能。
- 每个子进程总是带有三个流对象:child.stdin, child.stdout 和child.stderr。他们可能会共享父进程的 stdio 流,或者也可以是独立的被导流的流对象。
- Node 提供了 child_process 模块来创建子进程
二、创建进程的方法 - exec - child_process.exec 使用子进程执行命令,缓存子进程的输出,并将子进程的输出以回调函数参数的形式一次性返回。exec方法会从子进程中返回一个完整的buffer。默认情况下,这个buffer的大小应该是200k。如果子进程返回的数据大小超过了200k,程序将会崩溃,同时显示错误信息“Error:maxBuffer exceeded”。你可以通过在exec的可选项中设置一个更大的buffer体积来解决这个问题,但是你不应该这样做,因为exec本来就不是用来返回很多数据的方法。
- spawn - child_process.spawn 使用指定的命令行参数创建新进程。spawn 会返回一个带有stdout和stderr流的对象。你可以通过stdout流来读取子进程返回给Node.js的数据。stdout拥有’data’,’end’以及一般流所具有的事件。当你想要子进程返回大量数据给Node时,比如说图像处理,读取二进制数据等等,你最好使用spawn方法。
- fork - child_process.fork 是 spawn()的特殊形式,用于在子进程中运行的模块,如 fork(‘./son.js’) 相当于 spawn(‘node’, [‘./son.js’]) 。与spawn方法不同的是,fork会在父进程与子进程之间,建立一个通信管道,用于进程之间的通信。
实例分析
exec:
require('child_process').exec('dir', {encoding: ‘utf-8’}, function(err, stdout, stderr) {
if (err) {
console.log(error.stack);
console.log('Error code: ' + error.code);
console.log('Signal received: ' + error.signal);
}
//console.log(err, stdout, stderr);
console.log('data : ' + stdout);
}).on('exit', function (code) {
console.log('子进程已退出, 退出码 ' + code);
});
spawn:
var child_process = require('child_process');
var spawnObj = child_process.spawn('ping', ['127.0.0.1'], {encoding: 'utf-8'});
spawnObj.stdout.on('data', function(chunk) {
console.log(chunk.toString());
});
spawnObj.stderr.on('data', (data) => {
console.log(data);
});
spawnObj.on('close', function(code) {
console.log('close code : ' + code);
}
spawnObj.on('exit', (code) => {
console.log('exit code : ' + code);
fs.close(fd, function(err) {
if(err) {
console.error(err);
}
});
});
fork:
分为 “父进程”(parent.js) 和”子进程”(child.js)。在命令行执行的时候要切换到上述文件的目录中,否则会找不到子进程。
//parent.js
console.log('parent pid: ' + process.pid);
var fork = require('child_process').fork;
//fork方法返回的是子进程
var child = fork('./child.js');
console.log('fork return pid: ' + child.pid);
child.on('message', function(msg){
console.log('parent get message: ' + JSON.stringify(msg));
});
child.send({key: 'parent value'});
//child.js:
console.log('child pid: ' + process.pid);
process.on('message', function(msg){
console.log('child get message: ' + JSON.stringify(msg));
});
process.send({key: 'child value'});
15、什么是stub
• stub用于模块的行为。测试时,stub可以为函数调用返回模拟的结果。比如说,我们写文件时,实际上并不需要真正去写。
var fs = require('fs');
var writeFileStub = sinon.stub(fs, 'writeFile', function(path, data, cb)
{
return cb(null);
});
expect(writeFileStub).to.be.called;
writeFileStub.restore();
16、测试金字塔
• 测试金字塔反应了需要写的单元测试,集成测试以及端到端测试的比例:
• 测试HTTP接口时应该是这样的:
• 很多单元测试,分别测试各个模块(依赖需要stub)
• 较少的集成测试,测试各个模块之间的交互(依赖不能stub)
• 少量端到端测试,去调用真正地接口(依赖不能stub)