原文地址: https://codeburst.io/the-only-nodejs-introduction-youll-ever-need-d969a47ef219
做者: vick_onrails
摘要:这篇文章适合对Node一无所知或了解很少的初学者阅读。全面但不深刻地讲了包括http模块、express、mongodb和RESTful API等知识点。
若是你是前端开发工做者,那么对你来讲,基于NodeJS编写web程序已经不是什么新闻了。而无论是NodeJS仍是web程序都很是依赖JavaScript这门语言。javascript
首先,咱们要认识到一点:Node并非银弹。也就是说,它不是全部项目的最佳解决方案。任何人均可以基于Node建立一个服务器,可是这须要你对编写web程序的语言具备必定程序的有很深刻的理解。html
最近,我从学习Node的过程当中发现了许多乐趣,同时我也意识到我已经掌握了必定的知识,应该分享出来,而且从社区得到反馈来提高本身。前端
那么就让咱们开始吧。java
Web应用每每基于客户端/服务器模式,当客户端向服务器请求资源时,服务器会响应这个请求而且返回相应的资源。服务器只会在接收到客户端请求时才会作出响应,同时会在响应结束后关闭与客户端的链接。node
这种设计模式须要考虑到效率问题,由于每个请求都须要处理时间和资源。所以,服务器在每一次处理请求的资源后应该关闭这个链接,以便于响应其余请求。jquery
若是同时有成千上万个请求同时发往服务器,服务器会变成什么样子呢?当你问出这个问题时,你必定不想看到一个请求必须等待其余请求被响应后才能轮到他的情形,由于这段延迟实在是太长了。git
想象一下,当你想要打开FaceBook,但由于在你以前已经有上千人向服务器发出过请求,因此你须要等待5分钟才能看到内容。有没有一种解决方案来同时处理成百上千个请求呢?所幸咱们有线程这个工具。github
线程是系统可以并行处理多任务所使用的方式。每个发给服务器的请求都会开启一个新的线程,而每一个线程会获取它运行代码所须要的一切。web
这听上去很奇怪?让咱们来看看这个例子:ajax
想象餐馆里只有一个厨师提供食物,当食物需求愈来愈多,事情也会变得愈来愈糟。在以前的全部订单都被处理前,人们不得不等待很长时间。而咱们能想到的方法就是增长更多的服务员来解决这个问题,对吧?这样可以同时应付更多的顾客。
每个线程都是一个新的服务员,而顾客就是浏览器。我想理解这一点对你来讲并不困难。
可是这种系统有一个反作用,让请求数达到必定数量时,过多的线程会占用全部系统内存和资源。从新回到咱们的例子里,雇佣愈来愈多的人来供应食物必然会提升人力成本和占用更多的厨房空间。
固然,若是服务器在响应完客户端的请求后马上切断链接并释放全部资源,这对咱们来讲天然是极好的。
多线程系统擅长于处理CPU密集型操做,由于这些操做须要处理大量的逻辑,并且计算这些逻辑会花费更多的时间。若是每个请求都会被一个新的线程处理,那么主线程能够被解放出来去处理一些重要的计算,这样也能让整个系统变得更快。
译者注:经 @代码宇宙提醒,应该是多进程系统。因为原文使用的是thread,因此翻译成线程。下面的内容读者请自动脑补。
让主线程没必要忙于全部的运算操做是一种提升效率的好办法,可是能不能在此之上更进一步呢?
想象一下咱们如今已经有了一个多线程服务器,运行于Ruby on rails环境。咱们须要它读取文件而且发送给请求这个文件的浏览器。首先要知道的是Ruby并不会直接读取文件,而是通知文件系统去读取指定文件并返回它内容。顾名思义,文件系统就是计算机上一个专门用来存取文件的程序。
Ruby在向文件系统发出通知后会一直等待它完成读取文件的操做,而不是转头去处理其余任务。当文件系统处理任务完成后,Ruby才会从新启动去收集文件内容而且发送给浏览器。
这种方式很显然会形成阻塞的状况,而NodeJS的诞生就是为了解决这个痛点。若是咱们使用Node来向文件系统发出通知,在文件系统去读取文件的这段时间里,Node会去处理其余请求。而读取文件的任务完成后,文件系统会通知Node去读取资源而后将它返回给浏览器。事实上,这里的内部实现都是依赖于Node的事件循环。
Node的核心就是JavaScript和事件循环。
简单地说,事件循环就是一个等待事件而后在须要事件发生时去触发它们的程序。此外还有一点很重要,就是Node和JavaScript同样都是单线程的。
还记得咱们举过的餐厅例子吗?无论顾客数量有多少,Node开的餐厅里永远只有一个厨师烹饪食物。
与其余语言不一样,NodeJS不须要为每个请求开启一个新的线程,它会接收全部请求,而后将大部分任务委托给其余的系统。Libuv
就是一个依赖于OS内核去高效处理这些任务的库。当这些隐藏于幕后的工做者处理完委托给它们的事件后,它们会触发绑定在这些事件上的回调函数去通知NodeJS。
这儿咱们接触到了回调这个概念。回调理解起来并不困难,它是被其余函数看成参数传递的函数,而且在某种特定状况下会被调用。
NodeJS开发者们作的最多的就是编写事件处理函数,而这些处理函数会在特定的NodeJS事件发生后被调用。
NodeJS虽然是单线程,但它比多线程系统要快得多。这是由于程序每每并非只有耗时巨长的数学运算和逻辑处理,大部分时间里它们只是写入文件、处理网络请求或是向控制台和外部设备申请权限。这些都是NodeJS擅长处理的问题:当NodeJS在处理这些事情时,它会迅速将这些事件委托给专门的系统,转而去处理下一个事件。
若是你继续深刻下去,你也许会意识到NodeJS并不擅长处理消耗CPU的操做。由于CPU密集型操做会占用大量的主线程资源。对于单线程系统来讲,最理想的状况就是避免这些操做来释放主线程去处理别的事情。
还有一个关键点是在JavaScript中,只有你写的代码不是并发执行的。也就是说,你的代码每次只能处理一件事,而其余工做者,好比文件系统能够并行处理它们手头的工做。
若是你还不能理解的话,能够看看下面的例子:
好久之前有一个国王,他有一千个官员。国王写了一个任务清单让官员去作,清单很是很是很是长。有一个宰相,根据清单将任务委托给其余全部官员。每完成一项任务他就将结果报告给国王,以后国王又会给他另外一份清单。由于在官员工做的时候,国王也在忙于写其余清单。
这个例子要讲的是即便有不少官员在并行处理任务,国王每次也只能作一件事。这里,国王就是你的代码,而官员就是藏于NodeJS幕后的系统工做者。因此说,除了你的代码,每件事都是并行发生的。
好了,让咱们继续这段NodeJS之旅吧。
用NodeJS写一个web应用至关于编写事件回调。让咱们来看看下面的例子:
npm init
命令,一直回车直到你在文件夹根目录下建立了一个package.json文件。新建一个名为server.js的文件,复制并粘贴下面的代码:
//server.js const http = require('http'), server = http.createServer(); server.on('request',(request,response)=>{ response.writeHead(200,{'Content-Type':'text/plain'}); response.write('Hello world'); response.end(); }); server.listen(3000,()=>{ console.log('Node server created at port 3000'); });
在命令行中,输入node server.js
,你会看到下面的输出:
node server.js //Node server started at port 3000
打开浏览器而且进入localhost:3000
,你应该可以看到一个Hello world
信息。
首先,咱们引入了http模块。这个模块提供了处理htpp操做的接口,咱们调用createServer()
方法来建立一个服务器。
以后,咱们为request事件绑定了一个事件回调,传递给on方法的第二个参数。这个回调函数有2个参数对象,request表明接收到的请求,response表明响应的数据。
不只仅是处理request事件,咱们也可让Node去作其余事情。
//server.js const http = require('http'), server = http.createServer((request,response)=>{ response.writeHead(200,{'Content-Type':'text/plain'}); response.write('Hello world'); response.end(); }); server.listen(3000,()=>{ console.log('Node server created at port 3000'); });
在当面的代码里,咱们传给createServer()一个回调函数,Node把它绑定在request事件上。这样咱们只须要关心request和response对象了。
咱们使用response.writeHead()
来设置返回报文头部字段,好比状态码和内容类型。而response.write()
是对web页面进行写入操做。最后使用response.end()
来结束这个响应。
最后,咱们告知服务器去监听3000端口,这样咱们能够在本地开发时查看咱们web应用的一个demo。listen这个方法要求第二个参数是一个回调函数,服务器一启动,这个回调函数就会被执行。
Node是一个单线程事件驱动的运行环境,也就是说,在Node里,任何事都是对事件的响应。
前文的例子能够改写成下面这样:
//server.js const http = require('http'), makeServer = function (request,response){ response.writeHead(200,{'Content-Type':'text/plain'}); response.write('Hello world'); response.end(); }, server = http.createServer(makeServer); server.listen(3000,()=>{ console.log('Node server created at port 3000');
makeServer
是一个回调函数,因为JavaScript把函数看成一等公民,因此他们能够被传给任何变量或是函数。若是你还不了解JavaScript,你应该花点时间去了解什么是事件驱动程序。
当你开始编写一些重要的JavaScript代码时,你可能会遇到“回调地狱”。你的代码变得难以阅读由于大量的函数交织在一块儿,错综复杂。这时你想要找到一种更先进、有效的方法来取代回调。看看Promise吧,Eric Elliott 写了一篇文章来讲解什么是Promise,这是一个好的入门教程。
一个服务器会存储大量的文件。当浏览器发送请求时,会告知服务器他们须要的文件,而服务器会将相应的文件返回给客户端。这就叫作路由。
在NodeJS中,咱们须要手动定义本身的路由。这并不麻烦,看看下面这个基本的例子:
//server.js const http = require('http'), url = require('url'), makeServer = function (request,response){ let path = url.parse(request.url).pathname; console.log(path); if(path === '/'){ response.writeHead(200,{'Content-Type':'text/plain'}); response.write('Hello world'); } else if(path === '/about'){ response.writeHead(200,{'Content-Type':'text/plain'}); response.write('About page'); } else if(path === '/blog'){ response.writeHead(200,{'Content-Type':'text/plain'}); response.write('Blog page'); } else{ response.writeHead(404,{'Content-Type':'text/plain'}); response.write('Error page'); } response.end(); }, server = http.createServer(makeServer); server.listen(3000,()=>{ console.log('Node server created at port 3000'); });
粘贴这段代码,输入node server.js
命令来运行。在浏览器中打开localhost:3000
和localhost:3000/abou
,而后在试试打开localhost:3000/somethingelse
,是否是跳转到了咱们的错误页面?
虽然这样知足了咱们启动服务器的基本要求,可是要为服务器上每个网页都写一遍代码实在是太疯狂了。事实上没有人会这么作,这个例子只是让你了解路由是怎么工做的。
若是你有注意到,咱们引入了url这个模块,它能让咱们处理url更加方便。
为parse()方法传入一个url字符串参数,这个方法会将url拆分红protocol
、host
、path
和querystring
等部分。若是你不太了解这些单词,能够看看下面这张图:
因此当咱们执行url.parse(request.url).pathname
语句时,咱们获得一个url路径名,或者是url自己。这些都是咱们用来进行路由请求的必要条件。不过这件事还有个更简单的方法。
若是你以前作过功课,你必定据说过Express。这是一个用来构建web应用以及API的NodeJS框架,它也能够用来编写NodeJS应用。接着往下看,你会明白为何我说它让一切变得更简单。
在你的终端或是命令行中,进入电脑的根目录,输入npm install express --save
来安装Express模块包。要在项目中使用Express,咱们须要引入它。
const express = require('express');
欢呼吧,生活将变得更美好。
如今,让咱们用express进行基本的路由。
//server.js const express = require('express'), server = express(); server.set('port', process.env.PORT || 3000); //Basic routes server.get('/', (request,response)=>{ response.send('Home page'); }); server.get('/about',(request,response)=>{ response.send('About page'); }); //Express error handling middleware server.use((request,response)=>{ response.type('text/plain'); response.status(505); response.send('Error page'); }); //Binding to a port server.listen(3000, ()=>{ console.log('Express server started at port 3000'); });
译者注:这里不是很理解为何代码中错误状态码是505。
如今的代码是否是看上去更加清晰了?我相信你很容易就能理解它。
首先,当咱们引入express模块后,获得的是一个函数。调用这个函数后就能够开始启动咱们的服务器了。
接下来,咱们用server.set()
来设置监听端口。而process.env.PORT
是程序运行时的环境所设置的。若是没有这个设置,咱们默认它的值是3000.
而后,观察上面的代码,你会发现Express里的路由都遵循一个格式:
server.VERB('route',callback);
这里的VERB能够是GET、POST等动做,而pathname是跟在域名后的字符串。同时,callback是咱们但愿接收到一个请求后触发的函数。
最后咱们再调用server.listen()
,还记得它的做用吧?
以上就是Node程序里的路由,下面咱们来挖掘一下Node如何调用数据库。
不少人喜欢用JavaScript来作全部事。恰好有一些数据库知足这个需求,好比MongoDB、CouchDB等待。这些数据库都是NoSQL数据库。
一个NoSQL数据库以键值对的形式做为数据结构,它以文档为基础,数据都不以表格形式保存。
咱们来能够看看MongoDB这个NoSQL数据库。若是你使用过MySQL、SQLserver等关系型数据库,你应该熟悉数据库、表格、行和列等概念。 MongoDB与他们相比并无特别大的区别,不过仍是来比较一下吧。
译者注:这儿应该有个表格显示MongoDB与MySQL的区别,可是原文里没有显示。
为了让数据更加有组织性,在向MongoDB插入数据以前,咱们可使用Mongoose来检查数据类型和为文档添加验证规则。它看上去就像Mongo与Node之间的中介人。
因为本文篇幅较长,为了保证每一节都尽量的简短,请你先阅读官方的MongoDB安装教程。
此外,Chris Sevilleja写了一篇Easily Develop Node.js and MongoDB Apps with Mongoose,我认为这是一篇适合入门的基础教程。
API是应用程序向别的程序发送数据的通道。你有没有登录过某些须要你使用facebook帐号登陆的网页?facebook将某些函数公开给这些网站使用,这些就是API。
一个RESTful API应该不以服务器/客户端的状态改变而改变。经过使用一个REST接口,不一样的客户端,即便它们的状态各不相同,可是在访问相同的REST终端时,应该作出同一种动做,而且接收到相同的数据。
API终端是API里返回数据的一个函数。
编写一个RESTful API涉及到使用JSON或是XML格式传输数据。让咱们在NodeJS里试试吧。咱们接下来会写一个API,它会在客户端经过AJAX发起请求后返回一个假的JSON数据。这不是一个理想的API,可是能帮助咱们理解在Node环境中它是怎么工做的。
npm init
。这会建立一个收集依赖的文件;npm install --save express
来安装express;server.js
,index.html
和users.js
;//users.js module.exports.users = [ { name: 'Mark', age : 19, occupation: 'Lawyer', married : true, children : ['John','Edson','ruby'] }, { name: 'Richard', age : 27, occupation: 'Pilot', married : false, children : ['Abel'] }, { name: 'Levine', age : 34, occupation: 'Singer', married : false, children : ['John','Promise'] }, { name: 'Endurance', age : 45, occupation: 'Business man', married : true, children : ['Mary'] }, ]
这是咱们传给别的应用的数据,咱们导出这份数据让全部程序均可以使用。也就是说,咱们将users这个数组保存在modules.exports
对象中。
//server.js const express = require('express'), server = express(), users = require('./users'); //setting the port. server.set('port', process.env.PORT || 3000); //Adding routes server.get('/',(request,response)=>{ response.sendFile(__dirname + '/index.html'); }); server.get('/users',(request,response)=>{ response.json(users); }); //Binding to localhost://3000 server.listen(3000,()=>{ console.log('Express server started at port 3000'); });
咱们执行require('express')
语句而后使用express()
建立了一个服务变量。若是你仔细看,你还会发现咱们引入了别的东西,那就是users.js
。还记得咱们把数据放在哪了吗?要想程序工做,它是必不可少的。
express有许多方法帮助咱们给浏览器传输特定类型的内容。response.sendFile()
会查找文件而且发送给服务器。咱们使用__dirname
来获取服务器运行的根目录路径,而后咱们把字符串index.js
加在路径后面保证咱们可以定位到正确的文件。
response.json()
向网页发送JSON格式内容。咱们把要分享的users数组传给它当参数。剩下的代码我想你在以前的文章中已经很熟悉了。
//index.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Home page</title> </head> <body> <button>Get data</button> <script src="https://code.jquery.com/jquery-3.2.1.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script> <script type="text/javascript"> const btn = document.querySelector('button'); btn.addEventListener('click',getData); function getData(e){ $.ajax({ url : '/users', method : 'GET', success : function(data){ console.log(data); }, error: function(err){ console.log('Failed'); } }); } </script> </body> </html>
在文件夹根目录中执行node server.js
,如今打开你的浏览器访问localhost:3000
,按下按钮而且打开你的浏览器控制台。
在btn.addEventListent('click',getData);
这行代码里,getData经过AJAX发出一个GET请求,它使用了$.ajax({properties})
函数来设置url
,success
和error
等参数。
在实际生产环境中,你要作的不只仅是读取JSON文件。你可能还想对数据进行增删改查等操做。express框架会将这些操做与特定的http动词绑定,好比POST、GET、PUT和DELETE等关键字。
要想深刻了解使用express如何编写API,你能够去阅读Chris Sevilleja写的Build a RESTful API with Express 4。
计算机网络是计算机之间分享接收数据的链接。要在NodeJS中进行连网操做,咱们须要引入net
模块。
const net = require('net');
在TCP中必须有两个终端,一个终端与指定端口绑定,而另外一个则须要访问这个指定端口。
若是你还有疑惑,能够看看这个例子:
以你的手机为例,一旦你买了一张sim卡,你就和sim的电话号码绑定。当你的朋友想要打电话给你时,他们必须拨打这个号码。这样你就至关于一个TCP终端,而你的朋友是另外一个终端。
如今你明白了吧?
为了更好地吸取这部分知识,咱们来写一个程序,它可以监听文件而且当文件被更改后会通知链接到它的客户端。
node-network
;建立3个文件:filewatcher.js
、subject.txt
和client.js
。把下面的代码复制进filewatcher.js
。
//filewatcher.js const net = require('net'), fs = require('fs'), filename = process.argv[2], server = net.createServer((connection)=>{ console.log('Subscriber connected'); connection.write(`watching ${filename} for changes`); let watcher = fs.watch(filename,(err,data)=>{ connection.write(`${filename} has changed`); }); connection.on('close',()=>{ console.log('Subscriber disconnected'); watcher.close(); }); }); server.listen(3000,()=>console.log('listening for subscribers'));
接下来咱们提供一个被监听的文件,在subject.txt
写下下面一段话:
Hello world, I'm gonna change
而后,新建一个客户端。下面的代码复制到client.js
。
const net = require('net'); let client = net.connect({port:3000}); client.on('data',(data)=>{ console.log(data.toString()); });
最后,咱们还须要两个终端。第一个终端里咱们运行filename.js
,后面跟着咱们要监听的文件名。
//subject.txt会保存在filename变量中 node filewatcher.js subject.txt //监听订阅者
在另外一个终端,也就是客户端,咱们运行client.js
。
node client.js
如今,修改subject.txt
,而后看看客户端的命令行,注意到多出了一条额外信息:
//subject.txt has changed.
网络的一个主要的特征就是许多客户端均可以同时接入这个网络。打开另外一个命令行窗口,输入node client.js
来启动另外一个客户端,而后再修改subject.txt
文件。看看输出了什么?
若是你没有理解,不要担忧,让咱们从新过一遍。
咱们的filewatcher.js
作了三件事:
net.createServer()
建立一个服务器并向许多客户端发送信息。再来看一次filewatcher.js
。
//filewatcher.js const net = require('net'), fs = require('fs'), filename = process.argv[2], server = net.createServer((connection)=>{ console.log('Subscriber connected'); connection.write(`watching ${filename} for changes`); let watcher = fs.watch(filename,(err,data)=>{ connection.write(`${filename} has changed`); }); connection.on('close',()=>{ console.log('Subscriber disconnected'); watcher.close(); }); }); server.listen(3000,()=>console.log('listening for subscribers'));
咱们引入两个模块:fs和net来读写文件和执行网络链接。你有注意到process.argv[2]
吗?process是一个全局变量,提供NodeJS代码运行的重要信息。argv[]
是一个参数数组,当咱们获取argv[2]
时,但愿获得运行代码的第三个参数。还记得在命令行中,咱们曾输入文件名做为第三个参数吗?
node filewatcher.js subject.txt
此外,咱们还看到一些很是熟悉的代码,好比net.createServer()
,这个函数会接收一个回调函数,它在客户端链接到端口时触发。这个回调函数只接收一个用来与客户端交互的对象参数。
connection.write()
方法向任何链接到3000端口的客户端发送数据。这样,咱们的connetion
对象开始工做,通知客户端有一个文件正在被监听。
wactcher包含一个方法,它会在文件被修改后发送信息给客户端。并且在客户端断开链接后,触发了close事件,而后事件处理函数会向服务器发送信息让它关闭watcher中止监听。
//client.js const net = require('net'), client = net.connect({port:3000}); client.on('data',(data)=>{ console.log(data.toString()); });
client.js
很简单,咱们引入net模块而且调用connect方法去访问3000端口,而后监听每个data事件并打印出数据。
当咱们的filewatcher.js
每执行一次connection.write()
,咱们的客户端就会触发一次data事件。
以上只是网络如何工做的一点皮毛。主要就是一个端点广播信息时会触发全部链接到这个端点的客户端上的data事件。
若是你想要了解更多Node的网络知识,能够看看官方NodeJS的文档:net模块。你也许还须要阅读Building a Tcp service using Node。
好了,这就是我要讲的所有。若是你想要使用NodeJS来编写web应用程序,你要知道的不只仅是编写一个服务器和使用express进行路由。
下面是我推荐的一些书:
NodeJs the right way
Web Development With Node and Express
若是你还有什么看法,能够在下面发表评论。
译者注:这个翻译项目才开始,之后会翻译愈来愈多的做品。我会努力坚持的。
项目地址: https://github.com/WhiteYin/translation