基于node.js的web程序入门

本地安装配置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:8080http://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也是单线程的,但它可以通过事件轮询来实现并发操作。
下一个版本我们将解决这个问题,在这之前,我们可以先休息一下。

如果反应比较好,后面会更新(二)介绍非阻塞线程、数据库连接与增删改查、文件上传的实现!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值