本地安装配置node.js这里不讲,假设你已经安装好了这个环境,当然你要是真的没有安装好,请阅读:“node.js本地安装环境配置”。
这里也不会将关于node.js的历史,因为历史很短,但业绩却不差,总之,它很好用。
看过很多node.js的入门教学,哎,小的就是介绍hello world的页面输出,太简单,不够实用;大的就是给一个web工程的打包文件,初学者很难掌握。都不好。
这个教程是写给node.js的完全初学者的,甚至你可以是js的初学者。
我们一步一步来做,所谓“迭代”。
我们的这个web案例的名字就定为interneter,叫这个名字的原因就是我很喜欢的一个微信公众号就是这个名字。好吧,我承认,它是我维护的。
来吧,第一个版本的interneter工程,我们称它为interneter_1,在你的node.js的workspaces(自己找一个舒服的位置建立“workspaces”的文件夹)里建立一个“interneter_1”的文件夹。
比如我的workspaces在c盘根目录,这个位置真的很舒服。
node.js是运行在服务器端的javascript,看来它是帮助我们处理用户的web请求的。这就必须要有http的服务器环境,你之前可能是用apache的tomcat、ibm的websphere、或着weblogic、jboss或者其他的服务器,现在不用了。
我们用js来写一个服务器吧。
在刚才建立的文件夹里创建一个新的文件server.js
var http = require("http");
http.createServer(function(request, response){
response.writeHead(200,{"Content-Type":"text/plain"});
response.write("hello interneter");
response.end();
}).listen(8080);
console.log("interneter has started...");
写完之后保存。写完之后保存。写完之后保存。重要的事情说三遍。
这个程序的第一行获取了一个node.js给我们“内置”的一个http对象。我们把它获取之后变成这段程序的一个局部变量http。
这个http可以创建服务器,并监听8080端口(端口可以自己设置)。分别调用了createServer()和listen()方法。最后控制台输出了“interneter is started…”。
其中createServer这个方法里面的参数是一个方法(匿名函数),他直接返回了要创建的server的信息。这个方法传入了两个内置对象request和response,熟悉web的人一定对他不陌生。如果你对它们陌生,你就把它们当做,一次用户操作的输入参数列表和输出参数列表好了。第一行wiriteHead(),实际上就是写入输出的文本的状态和格式。第三行的end()表示回应结束。
好了,服务器创建完了。
进入你的interneter_1工程目录中,使用node server.js命令运行。
cmd命令行作为控制台输出了“interneter has started…”。
打开浏览器,推荐使用Chrome,因为node.js就是Google V8团队开发的,并且Chrome底层也使用到了node.js。
地址栏输入:http://127.0.0.1:8080,回车。
你看到了,浏览器页面输出了:hello interneter。实际上他执行了createServer()的第二行代码,response输出了这一行文字。
没错,上面就是类似hello world的程序教程,然而这并不够。
开始迭代。
接下来,我们复制interneter_1工程目录,重命名为interneter_2。
我们修改一下interneter_2的server.js文件。
我们把createServer()方法里的匿名函数,复制出来,给方法命名为“onRequest”。然后将方法名传入到createServer()的参数列表中。如下:
var http = require("http");
function onRequest(request, response){
response.writeHead(200,{"Content-Type":"text/plain"});
response.write("hello interneter");
response.end();
}
http.createServer(onRequest).listen(8080);
console.log("interneter has started...");
进入你的interneter_2工程目录中,使用node server.js命令运行。
浏览器地址栏输入:http://127.0.0.1:8080,回车。
你可以看到和interneter_1工程一样的效果,实际上两次的代码并没有什么区别。但是,通过interneter_2的代码我们看到了一个神奇的现象,就是js可以通过传入方法的名字“onRquest”,调用这个方法。这一点非常的重要,这对你理解下面的代码非常有帮助。
来,继续迭代。
我们复制interneter_2工程目录,重命名为interneter_3。
这次我们继续修改server.js文件,
我们在onRquest()的第一行添加一行代码:
onsole.log(“your request is received.”);
这样server.js就被修改为:
var http = require("http");
function onRequest(request, response){
console.log("your request has received.");
response.writeHead(200,{"Content-Type":"text/plain"});
response.write("hello interneter");
response.end();
}
http.createServer(onRequest).listen(8080);
console.log(“interneter is started…”);
进入你的interneter_3工程目录中,使用node server.js命令运行。
浏览器地址栏输入:http://127.0.0.1:8080,回车。
这时候你在回来看看cmd命令行。
发现多了两行输出,也就是现在的onRequest()第一行输出的内容:“your request has received.”。也就是说,当你回车发送给这个server一个请求之后,就会进入这个onRequest方法。
当你多次刷新页面,就会看到多个这样的输出。
至于为什么每次请求都会有两次输出,那是因为服务器在你访问http://127.0.0.1:8080/的时候会默认尝试读取http://127.0.0.1:8080/favicon.ico。它用来干什么?比如,当你访问了一个叫baidu的网站时,你会在标签栏上看到一个百度的logo,如下,取出这个ico文件的过程需要一个默认的请求。这就是那两行输出的第二行请求后面做的事情了。
interneter_3工程就这样了,它让我们了解了,用户的一次请求,或者一次刷新页面,点击链接就会来到这个server.js的onRequest()中。可是用户的请求类型那么多,网站要处理的模块不止一个,如果都在这样一个文件里面写,那是不是这个文件会写很长很多。看来我们需要新的方法来处理它们:分模块。
再一次迭代。
我们复制interneter_3工程目录,重命名为interneter_4。
我们知道,如果模块有多个的话,需要一个总的文件吧它们集合起来,声明到一个文件中,方便管理,我们不妨把它命名为main.js。
这样的你的interneter_4文件夹中不只有一个文件server.js,还有一个main.js文件。以后我们通过调用main.js启动服务器。
我们不着急写main.js文件。
需要继续修改server.js,我们现在server.js里面有处理请求的方法,但是如果想用main.js调用这个方法的话,main.js必须得到这个server,以及这个方法。
因此server.js:
var http = require("http");
function start(){
function onRequest(request, response){
console.log("your request has received.");
response.writeHead(200,{"Content-Type":"text/plain"});
response.write("hello interneter");
response.end();
}
http.createServer(onRequest).listen(8080);
console.log("interneter has started...");
}
exports.start = start;
我们把onRequest方法、http创建并监听端口、输出启动信息全部封装到start的方法里面,然后在最后一行导出了这个start。
这时候我们写main.js:
var server = require(“./server”);
server.start();
main.js的第一行获取了server,注意require()里面的参数即表示你需要引入的对象,而这个传入的参数中,“./”表示了当前的路径,“server”必须和你的“server.js”这个文件的文件名完全一致。
进入你的interneter_4工程目录中,使用node main.js命令运行。
注意:这次是运行main.js文件。
浏览器地址栏输入:http://127.0.0.1:8080,回车。
程序表现上都没有什么变化,请求响应和cmd控制台的输出依旧是那么的高效、迅速。
你肯定一点都不佩服你自己,不知道自己有多棒。现在你用js做了一件大事:跨文件传参。
让我们静一静,想想这个传参的过程:
在一个js文件里写一个函数,使用exports导出这个函数(只写函数名)变成一个变量,在另一个js使用require获取这个js文件成为一个对象,使用这个获取的对象调用导出的这个变量,就可以直接执行这个函数。
就是这样。
现在我们已经可以使用js建立服务器,并且回应用户的请求了。但是现在,一个最大的问题,我们并不知道用户给我们发送了一个什么的样的请求。比如,用户现在发送过来一个http://127.0.0.1:8080/add或者http://127.0.0.1:8080/delete,我们的程序没有办法看到用户的请求类型。
如果想要看到用户的请求名,我们需要建立main.js一个识别路径的功能,我们专业一点,把它称为:路由。
继续迭代。
我们复制interneter_4工程目录,重命名为interneter_5。
我们把这个路由模块文件命名为router.js,这样interneter_5文件夹里就有三个文件。
router.js先这样写:
function route(pathname){
console.log("about to route a request for "+pathname);
}
exports.route = route;
和之前一样,它声明了一个route方法,方法中输出了现在用户进行的请求名(pathname)。
然后再文件最后导出了这个route,你在想,这是为了在main.js中获取它吧?对,没错。
我们的main.js就只需要添加一行代码:
var router = require(“./router”);
变成了:
var server = require(“./server”);
var router = require(“./router”);
server.start(router.route);
我们把router对象里的这个route变量传到start()中。
然后我们在看看server.js怎么改。在这之前,我们需要了解另一个node.js的内置对象url,顾名思义,它封装了关于地址内容的一些操作,通过它我们可以request获取用户的请求路径url。server.js拿到这个从main.js里面获取的对象route,调用这个对象的方法route(),传入路径值。
server.js修改如下:
var http = require("http");
var url = require("url");
function start(route){
function onRequest(request, response){
console.log("your request has received.");
var pathname = url.parse(request.url).pathname;
route(pathname);
response.writeHead(200,{"Content-Type":"text/plain"});
response.write("hello interneter");
response.end();
}
http.createServer(onRequest).listen(8080);
console.log("interneter has started...");
}
exports.start = start;
这段代码中:var pathname = url.parse(request.url).pathname;
你可能会感到很奇怪,左边不用多说,一个声明变量。右边呢?
实际上,右边的连个url不是同一个东西,虽然写出来是一样的,第一个url是你第二行获取的url对象,它类似一个工具类,简单理解就是,这个对象里面有一些方法来处理一段url的字符串。而第二个url是封装在request内置对象里的成员变量url。
ok,你可能懂了。
按照之前的方法。
进入你的interneter_5工程目录中,使用node main.js命令运行。
浏览器地址栏输入:http://127.0.0.1:8080,回车。
回过头来看看cmd控制台输出:
它获取到了你输入的请求路径“/”。(注:http://127.0.0.1:8080和http://127.0.0.1:8080/一样)
同时你也看到了刚才我们说的,第二次默认请求标签页图标的过程,它的路径也被route()打印出来了。
这样,当你输入http://127.0.0.1:8080/addbook,回车或者http://127.0.0.1:8080/deletebook,回车。你在控制台就能看到,用户的不同请求了。
然而,你知道的,这个浏览器的地址栏使用户可以自己输入的,如果这个用户非常聪明,像你一样是个web程序员,他自己改了一个请求名,输入:http://127.0.0.1:8080/test,回车。这时候,你的工程并没有给/test请求写响应,然而你的router却也可以执行route()。看来我们得写一个处理器来过滤,或者说是判断用户的请求到底存不存在。
来,再来一次迭代。
我们复制interneter_5工程目录,重命名为interneter_6。
这一次,我们需要一个请求的处理器,来区别对待不同的请求,因此我们需要新建一个js文件,我们把它命名为requestHandlers.js,虽然我也觉得它名字很长,但是所完成的功能一目了然。
在requestHandlers.js里我们声明两个我们可以处理的请求addbook和deletebook:
其中,start方法是留给“/”请求的。因为默认的又“/”请求。
function start(){
console.log("request handler start() was called");
}
function addbook(){
console.log("request handler add() was called");
}
function deletebook(){
console.log("request handler delete() was called");
}
exports.start = start;
exports.addbook = addbook;
exports.deletebook = deletebook;
这个文件结构我就不再多讲了。不过要注意,这次我们在用户发出某一个请求之后,我们在控制台输出了用户执行了哪一个请求。
同样需要在main.js里面获取这个requestHandlers,然后把它交给start()去执行,判断用户传来一个什么样的请求。
为了过滤掉我们不处理的请求,我们需要把我们在rrequestHandlers.js里面声明的方法名存到一个数组中,这样当我们获取到用户输出的pathname之后,就可以在数组中需要找到这个声明的方法名。
main.js修改为:
var server = require("./server");
var router = require("./router");
var requestHandlers = require("./requestHandlers");
var handle = {};
handle["/"] = requestHandlers.start;
handle["/addbook"] = requestHandlers.addbook;
handle["/deletebook"] = requestHandlers.deletebook;
server.start(router.route, handle);
server.js是用户请求的之后,main.js转发过来这个请求执行server的start()的。server.js只修改了两行内容。即调用route(handle, pathname)多传了一个参数handle,获取这个参数也要添加到server的start()里面。
var http = require("http");
var url = require("url");
function start(route, handle){
function onRequest(request, response){
console.log("your request has received.");
var pathname = url.parse(request.url).pathname;
route(handle, pathname);
response.writeHead(200,{"Content-Type":"text/plain"});
response.write("hello interneter");
response.end();
}
http.createServer(onRequest).listen(8080);
console.log("interneter has started...");
}
exports.start = start;
这样我们就必须修改router.js里面的route(),让他能将传进来的用户请求路径pathname(从url中获取)和handle数组里面保存的我们实现的请求方法,匹配、判断这个方法到底在requestHandlers.js里存不存在。
router.js这么写:
function route(handle, pathname){
console.log("about to route a request for " + pathname);
if(typeof handle[pathname] === 'function'){
handle[pathname]();
}else{
console.log("no request found for" + pathname);
}
}
exports.route = route;
看了这段代码,你可能比较迷糊,主要是这几行新写的代码:
if(typeof handle[pathname] === 'function'){
handle[pathname]();
}else{
console.log("no request found for" + pathname);
}
我们看看这个if()条件吧,typeof handle[pathname] === ‘function’
假设用户传进来的请求是“/addbook”,那么这个pathname就是“/addbook”,这个handle[pathname]其实就是handle[“/addbook”],这样,我们结合main.js中声明好的,handle[“/addbook”]被赋予了是requestHandlers这个对象的addbook变量。也就是直接可以调用addbook这个方法。
因此typeof handle[pathname]就是检测handle[pathname]的类型的,一旦他是我们声明过的一个function(方法),那么它就可以执行,handlepathname就是来执行这个方法的。当server.js传进来的pathname是addbook时,执行addbook()。传进来的是pathname是deletebook就执行deletebook()。
如果传进来的是“test”,在handle[“/test”]上是匹配不到任何一个变量的,也就不会返回一个方法,因此就会执行else,输出这个请求没有找到。
进入你的interneter_6工程目录中,使用node main.js命令运行。
浏览器地址栏输入:http://127.0.0.1:8080,回车。
cmd控制台输出了“/”执行的start()。
输入:http://127.0.0.1:8080/addbook,回车。输入:http://127.0.0.1:8080/deletebook,回车。输入:http://127.0.0.1:8080/test,回车。
可以看到:
“/”请求正确执行了start()
“/addbook”请求正确执行了addbook()
“/deletebook”请求正确执行了deletebook()
“/test”请求未发现对应的方法
写到这里你一定累了,我们的代码逻辑也已经足够复杂。但这就是工程代码需要做到的:高内聚,低耦合。分层分模块,使得我们的工程在后面的维护过程中非常方便,而不是在一个文件大量代码中寻找问题。
然而,最后的这几个版本,我们观察结果。几乎都是在cmd的控制台中看结果,我们要做的是web项目,是面向浏览器的编程啊,因此我们现在要考虑页面的输出了。不要让它一直在输出这个无聊的hello interneter了,再说一次,interneter是我维护的一个微信公众号。
我们现在至少希望能狗在页面上显示出不同的输出语句,如果调用“/”,输出hello start,如果调用“/addbook”,输出hello addbook,如果调用“/deletebook”,输出hello deletebook,如果调用“/test”,输出404 not found。
再次迭代开发。
我们复制interneter_6工程目录,重命名为interneter_7。
interneter_7里面仍然还只有4个文件。
这次也只需要对requestHandlers.js,router.js以及server.js做很简单的修改就可以了。
在requestHandlers.js中,我们需要给每一个声明的方法添加一行代码,返回不同的“字符串”,以便后面用来输出,就好了。
所以requestHandlers.js:
function start(){
console.log("request handler start() was called");
return "hello start";
}
function addbook(){
console.log("request handler addbook() was called");
return "hello addbook";
}
function deletebook(){
console.log("request handler deletebook() was called");
return "hello deletebook";
}
exports.start = start;
exports.addbook = addbook;
exports.deletebook = deletebook;
同样,router.js也将匹配成功后,调用的该函数直接返回,实际上也就是上面requestHandlers.js里每个函数返回的“字符串”,如果没有匹配,则直接返回“404 not found”。这个字符串交由更高一层的server.js处理。
在server.js中,因为现在的route()是有返回值的,因此用一个变量去接受它,再把这个返回的字符串交给response对象去处理,输出到页面中。
var http = require("http");
var url = require("url");
function start(route, handle){
function onRequest(request, response){
console.log("your request has received.");
var pathname = url.parse(request.url).pathname;
var content = route(handle, pathname);
response.writeHead(200,{"Content-Type":"text/plain"});
response.write(content);
response.end();
}
http.createServer(onRequest).listen(8080);
console.log("interneter has started...");
}
exports.start = start;
这样的一个改动实际上非常的简单,也很容易理解。不同的函数返回不同的字符串,然后把它显示出来,这并不是一件困难的事情。
进入你的interneter_7工程目录中,使用node main.js命令运行。
浏览器地址栏输入:http://127.0.0.1:8080,回车。
输入:http://127.0.0.1:8080/addbook,回车。
输入:http://127.0.0.1:8080/deletebook,回车。
输入:http://127.0.0.1:8080/test,回车。
cmd后台的后台输出和interneter_6是一样的,因为我们并没有修改任何关于console.log()的代码。
现在非常棒的一点是我们,给服务器发送不一样的请求,已经能够得到不同的响应,展示出不同的页面了,所以一个网站最基本的东西似乎是已经做到了。
如果这样一个web程序放在网上让别人访问,假设它是一个非常棒的程序,今天上网的人都来访问它。那么它将会遇到前所未有的挑战:性能问题!
关于这个问题我们先用新版本的工程来做个试验。
我们复制interneter_7工程目录,重命名为interneter_8。
依然只需要这四个文件。
这次我们只改一个文件,就是真正的请求执行代码requestHandler.js
function start(){
console.log("request handler start() was called");
return "hello start";
}
function addbook(){
console.log("request handler addbook() was called");
function sleep(milliSeconds){
var startTime = new Date().getTime();
while(new Date().getTime() < startTime + milliSeconds);
}
sleep(10000);
return "hello addbook";
}
function deletebook(){
console.log("request handler deletebook() was called");
return "hello deletebook";
}
exports.start = start;
exports.addbook = addbook;
exports.deletebook = deletebook;
在这段程序中,我们在addbook()中添加了几行代码。添加了一个sleep()传进来一个毫秒数,当时间超过这个毫秒数,再跳出while循环。我们这样实际上模拟了一个执行较慢的操作,事实上在web应用中,一些数据量较大的查询、登录、添加、上传文件等操作,就是相对较慢,是完全有可能存在的问题。这次我们讲addbook()这个操作人为控制成10秒。
进入你的interneter_8工程目录中,使用node main.js命令运行。
一切都那么正常,如同一次漂亮的迭代。
这时候,请你在打开一个浏览器窗口,一定要是两个窗口。我们模拟两个用户同时使用这个应用。
一个输入:http://127.0.0.1:8080/addbook,先不要回车。
另一个输入:http://127.0.0.1:8080/deletebook,先不要回车。
就像这样:
确保输入正确,且还没有请求的。然后再输入/addbook请求的页面先回车,你会看到浏览器标题栏开始请求,图标在旋转。这时候它睡眠了。
你需要快速的到另一个窗口,请求/deletebook,不幸的是:
/deletebook这个窗口也睡眠了,而且也同时等了很久,是在/addbook这边完成执行之后,才有的响应。也就是说那个原本deletebook很快速的操作,必须要等到执行较慢的操作之后才开始执行。
你不妨再做一次试验,在启动/addbook第一个窗口开始睡眠之后,迅速到第二个窗口,一直按住F5刷新操作不松手,那么你可以发现addbook()操作再也完成不了了。试想一下,很多用户在用的应用,每秒钟都有人在应用里面操作,而登录是一个较慢的操作,很多人操作占据了这些资源,那么下一个先要登录的人,将会很难进入该应用。
一切的原因所在就是javascript的单线程!
也就是说,addbook()在执行过程在,阻塞的这个线程,别人也没有办法使用这个线程。
node.js也是单线程的,但它可以通过事件轮询来实现并发操作。
下一个版本我们将解决这个问题,在这之前,我们可以先休息一下。
如果反应比较好,后面会更新(二)介绍非阻塞线程、数据库连接与增删改查、文件上传的实现!