Node.js的HTTP

Node.js的HTTP

HTTP 服务

HTTP概念

超文本传输协议(HTTP)是用于从万维网服务器传输超文本到本地浏览器的传送协议。超文本传输协议(HTTP)是面向事务的(Transaction-oriented),应用层协议规定了在浏览器和服务器之间的请求和响应的格式和规则,它是万维网上能够可靠交换文件(包括文本、声音、图像等各种多媒体文件)的重要基础。

HTTP 工作原理

HTTP协议工作于客户端-服务端架构上。浏览器作为HTTP客户端通过URL向HTTP服务端即WEB服务器发送所有请求。

Web服务器有:Apache服务器,IIS服务器(Internet Information Services)等。

Web服务器根据接收到的请求后,向客户端发送响应信息。

HTTP默认端口号为80,但是你也可以改为8080或者其他端口。

服务器端基础概念

网站的组成

一个网站主要有三部分组成:

一、网站域名:访问网站所用的网址,选择简明好记,符合自身品牌的域名

二、网站程序:用户浏览网站所看到的页面和网站后台管理程序

三、网站空间:可以是虚拟主机或服务器,用于存储网站程序及资料,并提供网站程序运行所需要的环境。

具体步骤:
  1. 购买云服务器或者空间,根据自己所做的网站选定

  2. 购买一个域名

  3. 挑选源码,寻找自己想要的相关的程序

  4. 上传源码到服务器

网站应用程序

网站应用程序主要分为两大部分:客户端和服务器端

客户端:在浏览器中运行的部分,就是用户看到并与之交互的界面程序。使用HTML、CSS、JavaScript构建。

服务器端:在服务器中运行的部分,负责存储数据和处理应用逻辑。

img

网站服务器

网站服务器(Website Server)是指在互联网数据中心中存放网站的服务器。网站服务器主要用于网站在互联网中的发布、应用,是网络应用的基础硬件设施。

它能够提供网站访问服务的机器就是网站服务器,它能够接收客户端的请求,能够对请求做出响应。

可以简单的理解为一台电脑

以前放服务器的叫机房,现在都叫数据中心

服务器软件

服务器软件,服务器软件工作在客户端-服务器或浏览器-服务器的方式,有很多形式的服务器,常用的包括:文件服务器(File Server) - 如Novell的NetWare 。数据库服务器(Database Server) :如Oracle数据库服务器,MySQL,PostgreSQL,Microsoft SQL Server等。

应用环境

网站服务器可根据网站应用的需要,部署搭建ASP/JSP/.NET/PHP等应用环境。

流行两种环境:

  • 一种是Linux+Apache(Nginx)+Mysql+Php也就是LAMP/LNMP环境;

  • 一种是WINDOWS+IIS+ASP/.NET+MSSQL环境。

LAMP为现在使用最广的服务器环境,它运行在Linux系统下,稳定、安全,Apache是最著名的开源网页服务器,Mysql也是最著名的一种开源关系型数据库,而PHP是一门流行的开源脚本语言,能处理用户的动态请求。

Windows+IIS+ASP/.NET+MSSQL凭借其极强的易用性,也赢得了许多站长的青睐,Windows是著名的可视化操作系统,而IIS是运行在Windows上的Web服务器lemmaId=267249&ss_c=ssc.citiao.link),可使用ASP/.NET 两种编程语言开发,现在应用最广的就是ASP.NET。

Ip 地址

IP地址(Internet Protocol Address),全称为网际协议地址,是一种在Internet上的给主机编址的方式。它是IP协议提供的一种统一的地址格式,常见的IP地址分为IPv4与IPv6两大类,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异,是互联网中设备的唯一标识。举个例子来说:IP地址就相当于每个家庭的门牌号

Ip地址查询

因特网是全世界范围内的计算机连为一体而构成的通信网络的总称。连在某个网络上的两台计算机之间在相互通信时,在它们所传送的数据包里都会含有某些附加信息,这些附加信息其实就是发送数据的计算机的地址和接受数据的计算机的地址。人们为了通信的方便给每一台计算机都事先分配一个类似我们日常生活中的电话号码一样的标识地址,该标识地址就是IP地址。

查询本机IP

Windows 系统

开始→运行→输入cmd→回车,(也可用windows自带的快捷键,win+r)即打开了windows命令程序解释窗口(cmd.exe)→

接着输入ipconfig/all 后回车就能知道本机的IP地址信息,我们可以在命令提示符里面输入 ping 加上域名就可以查看域名网络状况了。若需要生成文本格式,则输入ipconfig/all >>存储地址:文件名.文件格式后缀。例如ipconfig/all >>d:IP.txt。这样就可以在D盘根目录生成一个包含本机IP地址信息的名为IP的TXT文件。

image.png

Unix/Linux

图形界面下Alt+Ctrl+Space →打开命令行终端→

方法1、输入:ifconfig

方法2、输入:ifconfig|grep “inet” |cut -c 0-36|sed -e ‘s/[a-zA-Z: ]//g’

方法3、输入:hostname-i

方法4、输入:netstat-r

方法5、输入:cat /etc/resolv.conf

→显示相关网络数据

其中inet addr为ip地址,HWaddr是主机的HardwareAddress即MAC。

查询本机外IP

如果电脑在路由器(或防火墙、代理服务器)后面,那么上述的方式就只能查询出来本地的IP地址,却无法查询出所在公网的IP地址。

查询本地的IP地址,使用户无法了解自己详细的网络状况。通过访问亚太互联网络信息中心IP查询网站来查询所在的公网IP地址。

静态IP和动态IP

静态IP

静态IP是指由你自己设定的IP。一般IP的设定主要有两种,baiDHCP服务器自动分配和用户自己指定。DHCP分配可以有效防止IP地址冲突。网关就是你这个IP所在网络向外面网络的出口设备。网络掩码是表示你的IP所属的网络编号的位数。DNS是将域名解析成IP地址的服务器。

静态分配IP地址是指给每一台计算机都分配一个固定的IP地址,优点是便于管理,特别是在根据IP地址限制网络流量的局域网中,以固定的IP地址或IP地址分组产生的流量为依据管理,可以免除在按用户方式计费时用户每次上网都必须进行的身份认证的繁琐过程,同时也避免了用户经常忘记密码的尴尬。

动态IP

动态IP地址(Dynamic IP)指的是在需要的时候才进行随机IP地址分配。

动态IP地址和静态IP地址是对应的,所谓动态就是指当你每一次上网时,电信会随机分配一个IP地址,静态指的是固定分配一个IP地址,每次使用都是这一个地址。

因为IP地址资源很宝贵,因此大部门用户上网时使用的IP地址都是动态的,好比通过Modem、ISDN、ADSL、有线宽频、小区宽频等方式上网的计算机,都是在每次上网的时候临时随机分配一个IP地址。

IP地址是一个32位二进制数的地址,理论上讲, 有大约40亿(2的32次方)个可能的地址组合,这好像是一个很庞大的地址空间。实际上,根据网络ID和主机ID的不同位数规则,可以将IP地址分为A (7 位网络ID和24位主机ID)、B (14位网络ID和16位主机ID)、C (21位网络ID和8 位主机ID)三类,因为历史原因和技术发展的差异,A 类地址和B 类地址几乎分配殆尽,能够供全球各国各组织分配的只有C 类地址。所以说IP地址是一种非常重要的网络资源。

简单的说,静态IP在断开网络连接,重连IP后仍是原来的IP地址,而动态IP断开重连后IP地址就变了。

静态IP和动态IP的区别

静态IP一般都是网络运营商提供的,或者是企业的静态不变的IP,只要上网,用的都是相同的IP,利用静态IP也更有利于管理。

但是如果使用静态IP就有可能产生IP地址冲突,因为如果一台机子一直用的是一个固定的IP,然后另一台机子把自己的IP也改成了以前一台机子相同的IP,这样就产生了IP地址冲突,可能导致某一方无法上网。而相对与动态IP来说,是不会有相同IP的问题的,因为每次IP都是自动分配的,不可能产生相同的IP地址,但是使用动态IP就不利于管理的,每一次IP都不同。

域名

域名(英语:Domain Name),又称网域,是由一串用点分隔的名字组成的Internet上某一台计算机或计算机组的名称,用于在数据传输时对计算机的定位标识(有时也指地理位置)。

由于IP地址具有不方便记忆并且不能显示地址组织的名称和性质等缺点,人们设计出了域名,并通过网域名称系统(DNS),Domain Name System)来将域名和IP地址相互映射,使人更方便地访问互联网,而不用去记住能够被机器直接读取的IP地址数串。

域名由两种基本类型组成:以机构性质命名的域和以国家地区代码命名的域。常见的以机构性质命名的域一般由三个字符组成

单个服务bai器怎么绑定多个域名

一、事前知识储备:

(1)普通绑定域名,假设域名为loclalhost
普通默认绑定的是80端口,这样在浏览器地址栏输入localhost就可以访问网站了
(2)绑定端口,域名为localhost,绑定端口为1216
这个在浏览器必须输入localhost:1216才能访问网站
(3)服务器一般用IIS或Apache,JSP有用Tomcat的。

二、IIS绑定域名和端口
A、windows2003+IIS6.0
  1. 服务器内点击【开始】->【管理工具】->【Internet 信息服务(IIS)管理器】,按照如下图示打开站点属性选项卡
  2. 在"网站标识"处点击【高级】
  3. 点击【添加】,之后在弹出的选项卡中的【主机头值】处填写需要绑定的域名即可,填写后点击确定
B、windows2008+IIS7.0

在Windows Server 2008上,IIS添加修改网站域名绑定,可参考如下步骤:

  1. 登录服务器->开始菜单->管理工具->打开【信息服务(IIS)管理器】。
  2. 在左侧导航栏点击【网站】,找到要修改的网站,点击右键后选择【编辑绑定】。
    如果需要在原有域名的基础上新添加域名绑定,点击【添加】添加新的域名。
    如果需要修改原来绑定的域名,选择相应的域名,点击【编辑】,打开之后进行修改。
C、windows2012+IIS7.0
  1. 打开iis管理器,展开节点,在网站处鼠标右键点击下,选择添加网站。
  2. 填写网站的信息,包括网站名称,路径,和主机名(域名)的信息,之后点击确定创建下站点即可。
  3. 创建好的站点可以参考下图,点击下站点,在右侧选择下绑定,之后点击下添加,可以给站点添加绑定下其他的域名。
二、Apache绑定域名和端口

Apache的配置文件一般放置在/etc/httpd/conf文件夹下,httpd.conf是它的主配置文件,在进行配置时可以将虚拟主机的配置文件单独配置,如取名为vhost.conf,然后再http.conf中加入一行包含的语句“Include /etc/httpd/conf/vhost.conf”即可将vhost.conf的配置文件包含进来。

目前在一台服务器上搭建多个网站的方法主要由以下几种:

1、基于IP地址

这种方法适用于一台服务器有多个IP的情况,这种方法最简单粗暴。但一般一个VPS只绑定一个公网IP(额外IP另外加钱),故此方法不过多介绍。

2、基于端口号

这种方法使用不同的端口号来识别不同的网站,实际访问时使用网址加端口号的方式来实现,如localhost:80,localhost:81,localhost:82,该方式配置后需要在网站后加上端口号来访问不同的网站,适用于网站域名短缺但服务器的端口号充足的情况,缺点是网站后需要加上端口号,不利于用户访问

3、基于主机名

这种方法使用不同的域名来区分不同的网站,所有的域名解析都指向同一个IP,Apache通过在HTTP头中附带的host参数来判断用户需要访问哪一个网站,如localhost.com,localhost2,localhost3,多数情况下多个网站架在一台服务器上均使用该方法,下面以CentOS6.5系统为例,说明如何配置基于主机名的Apache虚拟主机。

(一)在Apache的配置文件夹下新建vhost.conf,作为虚拟主机的配置文件,在其中编写虚拟主机的内容,先加入默认的文件头:
NameVirtualHost *:80
ServerName *
#DocumentRoot为默认情况下网站的目录
DocumentRoot /www/html

(二)之后再根据实际情况添加以下的配置:

#在任意的地址上监听80端口上的HTTP请求

#网站管理员的联系方式

ServerAdmin

#网站的目录

DocumentRoot /var/www/html/test3

#主机名,apache就是通过这个地址来识别不同的网站

ServerName localhost

#错误日志路径

ErrorLog logs/localhost-error_log

#访问日志路径

CustomLoglogs/localhost-access_log common

(三)服务器上有多少个网站,那么就分别配置多少份以上信息,并根据实际情况修改其中的内容,测试时服务器上的页面配置

(四)在http.conf文件中加入一行“Include/etc/httpd/conf/vhost.conf”,将vhost.conf文件内容包含进来

(五)在/etc/hosts文件中将网站的域名绑定到本地环回地址上:
127.0.0.1 localhost1

127.0.0.1 localhost2

127.0.0.1 localhost3
(六)最后使用service httpdreload重新加载配置文件或service httpd restart重启Apache进程即可

(七)测试不同的域名返回了不同的网站内容

端口

“端口” 是英文port的意译,可以认为是设备与外界通讯交流的出口。端口可分为虚拟端口和物理端口,其中虚拟端口指计算机内部或交换机路由器内的端口,不可见。例如计算机中的80端口、21端口、23端口等。物理端口又称为接口,是可见端口,计算机背板的RJ45网口,交换机路由器集线器等RJ45端口。电话使用RJ11插口也属于物理端口的范畴。

端口是指接口电路中的一些寄存器,这些寄存器分别用来存放数据信息、控制信息和状态信息,相应的端口分别称为数据端口、控制端口和状态端口。

电脑运行的系统程序,其实就像一个闭合的圆圈,但是电脑是为人服务的,他需要接受一些指令,并且要按照指令调整系统功能来工作,于是系统程序设计者,就把这个圆圈截成好多段,这些线段接口就叫端口(通俗讲是断口,就是中断),系统运行到这些端口时,一看端口是否打开或关闭,如果关闭,就是绳子接通了,系统往下运行,如果端口是打开的,系统就得到命令,有外部数据输入,接受外部数据并执行。

详细信息请参阅IANA

TCP端口和UDP端口

TCP端口

TCP :Transmission Control Protocol传输控制协议,TCP是一种面向连接(连接导向)的、可靠的、基于字节流传输层(Transport layer)通信协议,由IETF的RFC 793说明(specified)。在简化的计算机网络中,它完成第四层传输层所指定的功能,UDP是同一层内另一个重要的传输协议

UDP端口

UDP :User Datagram Protocol 用户数据报协议,UDP是OSI参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。UDP 协议基本上是IP协议与上层协议的接口。UDP协议适用端口分别运行在同一台设备上的多个应用程序。

端口分类

硬件端口

CPU通过接口寄存器或特定电路与外设进行数据传送,这些寄存器或特定电路称之为端口。

其中硬件领域的端口又称接口,如:并行端口串行端口等。

网络端口

在网络技术中,端口(Port)有好几种意思。集线器、交换机)、路由器的端口指的是连接其他网络设备的接口,如RJ-45端口、Serial端口等。我们 这里所指的端口不是指物理意义上的端口,而是特指TCP/IP协议中的端口,是逻辑意义上的端口。

软件端口

缓冲区

端口类型

TCP端口和UDP端口。由于TCP和UDP 两个协议是独立的,因此各自的端口号也相互独立,比如TCP有235端口,UDP也 可以有235端口,两者并不冲突。

1.周知端口(Well Known Ports)

周知端口是众所周知的端口号,范围从0到1023,其中80端口分配给WWW服务,21端口分配给FTP服务等。我们在IE的地址栏里输入一个网址的时候是不必指定端口号的,因为在默认情况下WWW服务的端口是“80”。

网络服务是可以使用其他端口号的,如果不是默认的端口号则应该在 地址栏上指定端口号,方法是在地址后面加上冒号“:”(半角),再加上端口号。比如使用“8080”作为WWW服务的端口,则需要在地址栏里输入“网址:8080”。

但是有些系统协议使用固定的端口号,它是不能被改变的,比如139 端口专门用于NetBIOS与TCP/IP之间的通信,不能手动改变。

2.动态端口(Dynamic Ports)

动态端口的范围是从49152到65535。之所以称为动态端口,是因为它 一般不固定分配某种服务,而是动态分配。

3.注册端口

端口1024到49151,分配给用户进程或应用程序。这些进程主要是用户选择安装的一些应用程序,而不是已经分配好了公认端口的常用程序。这些端口在没有被服务器资源占用的时候,可以用用户端动态选用为源端口。

端口 域名 和 IP的关系

80端口是为HTTP(HyperText Transport Protocol)即超文本传输协议开放的,此为上网冲浪使用次数最多的协议,主要用于WWW(World Wide Web)即万维网传输信息的协议。可以通过HTTP地址(即常说的“网址”)加“:80”来访问网站,因为浏览网页服务默认的端口号都是80,因此只需输入网址即可,不用输入“:80”了。

8080端口同80端口,是被用于WWW代理服务的,可以实现网页浏览,经常在访问某个网站或使用代理服务器的时候,会加上“:8080”端口号。另外Apache Tomcat web server安装后,默认的服务端口就是8080。试验性的页面服务多在8080号端口运行。

**域名不能指向端口。**域名解析如果要设置到非80端口,就需要使用隐藏的域名转发

域名解析时不识别端口的,其实就是将域名与这个IP地址绑定了,然后http访问时默认使用的是80端口,所以你若是进行了81端口的映射,那么你就要这样访问www.domain2.com:81

说白了,要看你服务器监听的是什么端口,默认是80端口,则不用手动加:80,如果服务器监听非80端口,那么则需要加上非80端口号。

如果你想让你的主机默认访问tomcat的服务,3个办法:(前提是80端口未占用)

  1. 把tomcat默认监听端口改为80,修改配置文件很容易完成。

  2. 再部署一个监听80端口的web server,比如apache, iis之类的, 这个web server的默认主页面加上自动跳转的代码,直接跳转到8080端口

  3. 部署apache+tomcat方式,对jsp的访问,都转向到tomcat server,这个设置起来麻烦一些。

    一个服务器也可监听多个端口,对多个端口形成映射。

注意:

  1. 一个服务器上可以存放很多个不同域名的网站,都可以使用相同的80端口,他们是不同域名绑定解析到了同一个服务器IP地址不同目录

  2. 一个服务器上可以存放很多个二级域名的网站,都可以使用相同的80端口,他们是不同二级域名绑定解析到了同一个服务器IP地址的不同目录

  3. 域名只是对应IP

  4. 域名如果不加端口的话,http请求默认是80端口,https默认是443。

url

统一资源定位系统(uniform resource locator;URL)是因特网的万维网服务程序上用于指定信息位置的表示方法。它最初是由蒂姆·伯纳斯·李发明用来作为万维网的地址。现在它已经被万维网联盟编制为互联网标准RFC1738。

URL 格式:传输协议://服务器IP或域名:端口/资源所在位置标识

https://blog.csdn.net/mynewdays/article/details/103814431

  • 传输协议:https
  • 服务器域名:blog.csdn.net,端口默认为80
  • 资源所在位置标识:mynewdays/article/details/103814431
image.png

API

API(Application Programming Interface,应用程序接口)是一些预先定义的接口(如函数、HTTP接口),或指软件系统不同组成部分衔接的约定。 用来提供应用程序与开发人员基于某软件或硬件得以访问的一组例程,而又无需访问源码,或理解内部工作机制的细节。

API分类

Windows API

API函数包含在Windows系统目录下的动态连接库文件中。Windows API是一套用来控制Windows的各个部件的外观和行为的预先定义的Windows函数。用户的每个动作都会引发一个或几个函数的运行以告诉Windows发生了什么。这在某种程度上很像Windows的天然代码。而其他的语言只是提供一种能自动而且更容易的访问API的方法。当你点击窗体上的一个按钮时,Windows会发送一个消息给窗体,VB获取这个调用并经过分析后生成一个特定事件。

更易理解来说:Windows系统除了协调应用程序的执行、内存的分配、系统资源的管理外,同时他也是一个很大的服务中心。调用这个服务中心的各种服务(每一种服务就是一个函数)可以帮助应用程序达到开启视窗、描绘图形和使用周边设备等目的,由于这些函数服务的对象是应用程序,所以称之为Application Programming Interface,简称API 函数。WIN32 API也就是MicrosoftWindows 32位平台的应用程序编程接口。

凡是在 Windows工作环境底下执行的应用程序,都可以调用Windows API。

linux API

在linux中,用户编程接口API遵循了UNIX中最流行的应用编程界面标准—POSIX标准。POSIX标准是由IEEE和ISO/IEC共同开发的标准系统。该标准基于当时现有的UNIX实践和经验,描述了操作系统的系统调用编程接口API,用于保证应用程序可以在源程序一级上在多种操作系统上移植运行。这些系统调用编程接口主要是通过C库(LIBC)来实现的。

Linux与Windows下路径分隔符
在读取文件路径时:

在Windows下的路径分隔符和Linux下的路径分隔符是不一样的

在Windows下

windows 系统中目录分隔符使用 “\

在linux系统下

在linux系统中路径中使用“/”

注意:

编写创建文件的代码应注意一点,在windows系统中路径中使用“\”,同时需要再加一个转义的“\”,即形成了类似如下的路径:

“path\fileName”

此种路径在windows系统没什么不对,但是到了linux系统会出现问题,在linux系统会生成名为“path\”的一个文件夹,当你再需要对创建的文件操作时,就会找不到文件。

解决时可以在路径中使用“/”,如:

“path/fileName”

但是某些时候,这样会在windows系统中出现问题,如:页面上点击按钮,action里在路径中用“/”来生成一个.csv文件,然后读出文件,在新窗口中打开。此时会出现在原窗口打开的现象,不会在新窗口中打开,如果在路径中用“\”,可以解决这个问题,但是,运行在linux上会出现开始说的情况。

最终的解决方法是:在路径中加入的路径分隔符随系统改变,即用File.separator,可以解决掉此问题。

File myFile = new File("C:" + File.separator + "tmp" + File.separator, "test.txt");
在读取http路径时
在Windows下

windows 系统中目录分隔符使用 ‘/’ 或者 “\” 都行

在linux系统下

在linux系统中路径中使用“/”

所以不建议开发者自己写死目录分隔符

另外,读取文件的时候推荐使用绝对路径(G:\2019级_online\20200122node\code)

如果要考虑到跨平台,最好使用凭借字符串的方法

image.png

开发设置

开发阶段,我们的电脑就是客户端,但是没有必要再购买一台电脑作为服务器

我们的电脑,既可以做客户端,也可以做服务器

当然需要安装服务器软件,这里是Node服务

前面学过,从一台电脑访问另一台电脑,需要知道另一台电脑的IP地址或者域名(这个需要申请和购买),如果我们自己的电脑作为服务器,那么如何访问呢

域名:localhost

IP:127.0.0.1

创建web服务器

node 是一门服务器语言,相较于前面学过的 php ,能做的事情更多

php能做网站,node 也可以

php 不能做的,node 也可以

这里的web服务器,与上面的服务器是两个概念

上面的服务器是一台电脑

这里的服务器是在电脑上启动的一个服务,作用是让当前电脑中的资源可以被其他电脑通过URL访问

创建web服务器的步骤

第一,引入http核心模块
const http = require('http');
第二, 创建一个server实例
const app = http.createServer();
第三, 监听request事件,当request事件被触发时,执行一个回调
app.on('request', (req, res) => {
  console.log('hello nodejs');
})
第四,绑定端口号,启动服务器
app.listen(3000, () =>{
    console.log('服务成功启动:http://127.0.0.1:3000');
})
编写一个能够在服务器上启动web服务器的文件
// 引入http模块
const http = require('http')
// 创建一个http服务
var app = http.createServer()
/*为app对象注册 request事件,当客户端有请求到达服务器后,会触发这个事件执行
回调函数中有两个参数
1)req:表示客户端的请求信息,可以从中获取客户端的信息
2)res:表示服务端的相应,可以使用此对象向客户端发送信息
*/
app.on('request', (req, res) => {
    // 服务器必须向客户端发送响应信息,否则客户端就会一直等待
    res.end('ok')
})
// 为服务区设置监听端口
app.listen(3000, () => {
    console.log('Server is running at http://127.0.0.1:3000')
})

http.createServer创建http服务

语法:

http.createServer([options][, requestListener])
  • 第二个参数requestListener,是一个自动添加到'request'事件的方法。返回一个新的 http.Server实例。

之前代码是使用server.on(‘request’,callback)来监听请求事件,由于http.createServer第二个参数也是个request监听请求事件。所以可以直接把request的请求事件的监听函数callback传递给http.createServer的第二个参数即可。

创建http服务实现不同请求,响应不同内容

例如下面的代码:

// 引入系统模块 http
const http=require('http')

//创建 http 服务
const app=http.createServer()
// 监测客户端的请求,并给予响应
app.on('request',function(req,res){
    console.log(req.url);
    if(req.url=='/index' || req.url=='/'){
        res.end('home')
    }else if(req.url=='/order'){
        res.end('order')
    }else if(req.url=='/my'){
        res.end('my')
    }else{
        res.end('sorry')
    }
})
// 启动服务
app.listen(3000,function(){
    console.log('服务成功启动:http://127.0.0.1:3000');
})

不同的url响应不同的内容:

  • 请求 / 或 /index,输出index内容
  • 请求 /order,输出order内容
  • 请求 /my,输出my内容

启动服务,输入http://127.0.0.1:3000/index 会响应 index 内容

总结:

  • 前面学习 html、css 和 js 时,我们安装了一个插件 live server,负责接受用户的请求,并将请求的数据返回给客户端
  • 现在我们自己编写一个类似 liver server 的服务入程序,作用也是接受客户端的请求,并进行想要的相应

HTTP请求

超文本传输协议(Hypertext Transfer Protocol,简称HTTP)是应用层协议。HTTP 是一种请求/响应式的协议,即一个客户端与服务器建立连接后,向服务器发送一个请求;服务器接到请求后,给予相应的响应信息。

请求

当我们建立好服务器,那么这时候我们的客户端也就是浏览器要去服务器中获取数据,或者向服务器发送数据时,应当如何操作?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TyZNvTrV-1612275032565)(http://qiniu.yaoyao.info/image-20200303111951567.png)]

具体来说分为三个步骤

  • DSN 解析,建立TCP连接,发送http请求
  • server接收到http请求,处理,并返回响应
  • 客户端接收到返回数据,并处理数据,如渲染页面,执行Js

两台计算机之间对话也要遵循一定的格式,就好比写信,要遵循一定的格式

img

超文本传输协议(英文:HyperText Transfer Protocol,缩写:HTTP)规定了如何从网站服务器传输超文本到本地浏览器,它基于客户端服务器架构工作,是客户端(用户)和服务器端(网站)请求和应答的标准。

HTTP 协议就是计算机之间对话要遵循的格式(协议)

报文

在HTTP请求和响应的过程中传递的数据块就叫报文,包括要传送的数据和一些附加信息,并且要遵守规定好的格式。

img

从客户端发往服务器的叫请求报文

从服务器返回给客户端的叫响应报文

从浏览器的开发者工具中可以查看报文信息

报文结构:

image.png

报文格式

客户端发送一个HTTP请求到服务器的请求消息包括以下格式:请求行(request line)、请求头部(header)、空行和请求数据四个部分组成,下图给出了请求报文的一般格式。img

请求方式

请求行

请求行由三部分组成:请求方法请求URL(不包括域名),HTTP协议版本

请求方法比较多:GET、POST、HEAD、PUT、DELETE、OPTIONS、TRACE、CONNECT

所有请求方法名称全为大写,目前有9种:

image.png

1) GET

传递参数长度受限制,因为传递的参数是直接表示在地址栏中,而特定浏览器和服务器对url的长度是有限制的。

因此,GET不适合用来传递私密数据,也不适合拿来传递大量数据。

一般的HTTP请求大多都是GET。

2)POST

POST把传递的数据封装在HTTP请求数据中,以名称/值的形式出现,可以传输大量数据,对数据量没有限制,也不会显示在URL中。

表单的提交用的是POST。

3)HEAD

HEAD跟GET相似,不过服务端接收到HEAD请求时只返回响应头,不发送响应内容。所以,如果只需要查看某个页面的状态时,用HEAD更高效,因为省去了传输页面内容的时间。

4)DELETE

删除某一个资源。

5)OPTIONS

用于获取当前URL所支持的方法。若请求成功,会在HTTP头中包含一个名为“Allow”的头,值是所支持的方法,如“GET, POST”。

6)PUT

把一个资源存放在指定的位置上。

本质上来讲, PUT和POST极为相似,都是向服务器发送数据,但它们之间有一个重要区别,PUT通常指定了资源的存放位置,而POST则没有,POST的数据存放位置由服务器自己决定。

关于POST和PUT的区别以及请求方法的幂等性,请参考文章:http的7种请求方法和幂等性

7)TRACE

回显服务器收到的请求,主要用于测试或诊断。

8)CONNECT

CONNECT方法是HTTP/1.1协议预留的,能够将连接改为管道方式的代理服务器。通常用于SSL加密服务器的链接与非加密的HTTP代理服务器的通信。

虽然 HTTP 的请求方式有 8 种,但是我们在实际应用中常用的也就是 getpost,其他请求方式也都可以通过这两种方式间接的来实现。

  • get:请求数据

    由于GET请求直接被嵌入在路径中,URL是完整的请求路径,包括了?后面的部分,因此你可以手动解析后面的内容作为GET请求的参数。

    get 请求会将参数附加在地址栏中的请求地址之后,参数通过键值对传递

    http://127.0.0.1:3000/index?name=yhb&age=18

    通过node中的url模块

    img

    输出结果

    img

    注:因为此时的url变成了:http://127.0.0.1:3000/index?name=yhb&age=18

  • post:发送数据

     /**
         * 获取以 post 方式提交的数据,需要用到 data和end 两个事件
         * 在 data 事件中不断获取提交的数据,进行累加
         * 当客户端提交的数据全部获取后,会触发 end 事件
         * 1)为请求对象添加两个事件监听:data和end
         * 2)因为post方式传递的数据两可能比较大,所以数据可能不是一次性传递过来的,每当接收到请求体中的数据,都会触发data事件
         * 3)当数据接受完毕后,就会触发end 事件
         * 4)所以,我们的思路是,声明一个变量,在 data 事件中累加变量,在end事件中,将其解析为真正的post请求格式
    
         */
    let postData = "";
    req.on("data", (params) => {
        postData += params;
    });
    req.on("end", () => {
        // 读取 data.json 文件,将字符串解析成 json 对象
        let blogs = JSON.parse(fs.readFileSync(file_path, "utf8"));
        blogs.push(JSON.parse(postData));
         // 转换成字符串写入
        fs.writeFileSync(file_path, JSON.stringify(blogs), "utf8");
        res.end("success");
    });
    

    post发送数据总结

    • 参数防止在请求体重进行传输
    • 获取post参数使用data和end事件
    • 使用querystring模块将参数转换为对象格式

请求地址:request url

 app.on('request', (req, res) => {
     req.headers  // 获取请求报文
     req.url      // 获取请求地址
     req.method   // 获取请求方法
 });

利用上面的代码,编写如下案例

const http = require('http')
// 创建http服务
const app = http.createServer()
app.on('request', (req, res) => {
    let params = url.parse(reeq.url,true)
    // 获取参数对象
    let query = params.query
    // 获取去除请求参数的请求路径
    let pathname = params.pathname
    console.log(query.name,query.age)
    if (pathname === '/' || pathname === '/index') {
        res.end('home')
    } else if (pathname === '/download') {
        res.end('download')
    } else if (pathname === '/list') {
        res.end('list')
    } else {
        res.end('sorry,i cannot understander you')
    }
})
app.listen(3000, function () {
    console.log('服务器启动')
})

报文响应

HTTP响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。

img

状态码:由3位数字组成,第一个数字定义了响应的类别

  1. 1xx:指示信息,表示请求已接收,继续处理

  2. 2xx:成功,表示请求已被成功接受,处理。

    • 200 OK:客户端请求成功
    • 204 No Content:无内容。服务器成功处理,但未返回内容。一般用在只是客户端向服务器发送信息,而服务器不用向客户端返回什么信息的情况。不会刷新页面。
    • 206 Partial Content:服务器已经完成了部分GET请求(客户端进行了范围请求)。响应报文中包含Content-Range指定范围的实体内容
  3. 3xx:重定向

    • 301 Moved Permanently:永久重定向,表示请求的资源已经永久的搬到了其他位置。

    • 302 Found:临时重定向,表示请求的资源临时搬到了其他位置

    • 303 See Other:临时重定向,应使用GET定向获取请求资源。303功能与302一样,区别只是303明确客户端应该使用GET访问

    • 307 Temporary Redirect:临时重定向,和302有着相同含义。POST不会变成GET

    • 304 Not Modified:表示客户端发送附带条件的请求(GET方法请求报文中的IF…)时,条件不满足。返回304时,不包含任何响应主体。虽然304被划分在3XX,但和重定向一毛钱关系都没有
      一个304的使用场景:
      缓存服务器向服务器请求某一个资源的时候,服务器返回的响应报文具有这样的字段:Last-Modified:Wed,7 Sep 2011 09:23:24,缓存器会保存这个资源的同时,保存它的最后修改时间。下次用户向缓存器请求这个资源的时候,缓存器需要确定这个资源是新的,那么它会向原始服务器发送一个HTTP请求(GET方法),并在请求头部中包含了一个字段:If-Modified-Since:Wed,7 Sep 2011 09:23:24,这个值就是上次服务器发送的响应报文中的最后修改时间。

      假设这个资源没有被修改,那么服务器返回一个响应报文:

HTTP/1.1 304 Not Modified
    Date:Sat, 15 Oct 2011 15:39:29
    (空行)                                      
    (空响应体)

用304告诉缓存器资源没有被修改,并且响应体是空的,不会浪费带宽。

  1. 4xx:客户端错误
  • 400 Bad Request:客户端请求有语法错误,服务器无法理解。
  • 401 Unauthorized:请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用。
  • 403 Forbidden:服务器收到请求,但是拒绝提供服务
  • 404 Not Found:请求资源不存在。比如,输入了错误的url
  • 415 Unsupported media type:不支持的媒体类型
  1. 5xx:服务器端错误,服务器未能实现合法的请求。
  • 500 Internal Server Error:服务器发生不可预期的错误。
  • 503 Server Unavailable:服务器当前不能处理客户端的请求,一段时间后可能恢复正常

到底返回什么状态码,完全由开发者根据实际的情况决定

img

内容类型

Content-Type解释
text/htmlhtml格式
text/plain纯文本格式
text/cssCSS格式
text/javascriptjs格式
image/gifgif图片格式
image/jpegjpg图片格式
image/pngpng图片格式
application/x-www-form-urlencodedPOST专用:普通的表单提交默认是通过这种方式。form表单数据被编码为key/value格式发送到服务器。
application/jsonPOST专用:用来告诉服务端消息主体是序列化后的 JSON 字符串
text/xmlPOST专用:发送xml数据
multipart/form-data用以支持向服务器发送二进制数据,以便可以在 POST 请求中实现文件上传等功能

image.png

内容类型的作用是告诉客户端(浏览器)返回的内容是什么类型,浏览器会根据这个类型决定如何渲染返回的数据

如下面的代码设置返回的内容为纯文本,那么浏览器就不会解释h2标签,而是直接当作纯文本显示

app.on('request', (req, res) => {
    res.writeHeader(200,{
        'content-type':'text/plain'
    })
    res.end('<h2>hello</h2>')
})

如果设置为 text/html,浏览器就会渲染h2标签了

app.on('request', (req, res) => {
    res.writeHeader(200,{
        'content-type':'text/html'
    })
    res.end('<h2>hello</h2>')
})

如果返回的内容中包含中文,会产生乱码问题,需要在内容类型后面指定编码

img

HTTP响应

路由

路由文件:接受用户的请求,并进行返回数控

在文件处理中,我们为了不让代码显得赘余,并且为了后期的维护和修改,我们经常将相同的代码分别封装在各个文件中,而路由文件就是其中之一

他们经常通过CommonJS或者ES6 模块把路由文件暴漏给执行文件

路由文件代码案例:

/**
 * 路由文件:接受用户的请求,并进行返回数控
 */

// 引入 blog 控制器
const {
    getList,
    getDetail,
    newBlog,
    updateBlog,
    deleteBlog,
} = require("../controller/blog");
const { SuccessModel, ErrorModel } = require("../model/resModel");
const handlerBlog = (req) => {
    // 使用 URL 模块对 req.url 进行封装
    let myUrl = new URL(req.url, "http://127.0.0.1:3010/");
    let method = req.method;
    let pathname = myUrl.pathname;
    // console.log(pathname);
    let msgResult = null;
    if (pathname == "/api/blog/list" && method == "GET") {
        msgResult = getList();
        // then 方法的返回值也是一个 promise
        return msgResult.then((data) => {
            return new SuccessModel("", data);
        });
    } else if (pathname == "/api/blog/detail" && method == "GET") {
        let id = myUrl.searchParams.get("id");
        msgResult = getDetail(id);
        return msgResult.then((data) => {
            return new SuccessModel("", data);
        });
    } else if (pathname == "/api/blog/new" && method == "POST") {
        msgResult = newBlog(req.body);
        // let postData = {title:'标题',content:'内容'}
        // msgResult = blog_ctr.newBlog(postData)
        return msgResult.then((data)=>{
            return new SuccessModel("", data);
        })
        
    } else if (pathname == "/api/blog/update" && method == "POST") {
        // msgResult = true;
        let id = myUrl.searchParams.get("id");
        let postData = req.body;
        msgResult = updateBlog(id, postData);
        return msgResult.then((data)=>{
            return new SuccessModel("", data);
        })
        
    } else if (pathname == "/api/blog/del" && method == "GET") {
        let id = myUrl.searchParams.get("id");
        msgResult = deleteBlog(id);
        return msgResult.then((data)=>{
            return new SuccessModel("", data);
        })
       
    }
    return msgResult;
};
// 暴漏 handlerBlog  方法
module.exports = {
    handlerBlog
};

静态资源

服务器端不需要处理,可以直接响应给客户端的资源就是静态资源,例如CSS、JavaScript、image、html文件

注意:服务器上所有的静态资源,如 html、css、js或者图片等文件,不能直接获取,必须编写代码读取此文件再返回给客户端

静态资源的编写

首先搭建环境,初始化环境

npm init -y

引入我们所需要的模块

// 引入 http 模块
const http = require("http");
// 引入读写文件模块
const fs = require("fs");
// 引入路径模块
const path = require("path");
// 引入 url 模块,用来处理 url 路径
const url = require("url");

创建http服务

// 创建http服务
const app = http.createServer();

监听所有客户端发送的请求,并基于响应

参数 req:存储的是所有有关请求的信息

参数 res:用于向客户端发送响应的对象

服务器上所有的静态资源,如 html、css、js或者图片等文件,不能直接获取,必须

编写代码读取此文件再返回给客户端

1.在此我们需要一个安装一个mime 模块,用来获取请求资源的 Mime 类型

安装命令:

npm install mime

然后引入

// 引入 mime 模块
const mime = require("mime");

获取请求资源的 Mime 类型

const mime_arr = [
    "image/jpeg",
    "image/png",
    "application/javascript",
    "text/css",
    "text/html",
  ];
  // 获取请求资源的 Mime 类型

app.on("request", (req, res) => {})请求时,会出现两条请求:

第一条时URL地址为用户输入的客户端请求的目标URL地址,"/"代表用户的目标url地址为web应用程序的根目录.

第二个目标URL地址问浏览器为页面在收藏夹中的显示图标.默认为favicon.ico.而自动发出的请求的目标URL地址.

所以为了不必要的麻烦,我们直接这样写排除

不必要的请求

app.on("request", (req, res) => {
  // 排除不必要的请求
  if (req.url == "/favicon.ico") {
    return;
  }
})

app.js代码的完整代码

// 引入 http 模块
const http = require("http");
// 引入读写文件模块
const fs = require("fs");
// 引入路径模块
const path = require("path");
// 引入 mime 模块
const mime = require("mime");
// 引入 moment 模块
const moment = require("moment");
// 引入 url 模块,用来处理 url 路径
const url = require("url");

// 创建http服务
const app = http.createServer();
/**
 * 监听所有客户端发送的请求,并基于响应
 * 参数 req:存储的是所有有关请求的信息
 * 参数 res:用于向客户端发送响应的对象
 *
 * 服务器上所有的静态资源,如 html、css、js或者图片等文件,不能直接获取,必须
 * 编写代码读取此文件再返回给客户端
 */

app.on("request", (req, res) => {
  // 排除不必要的请求
  if (req.url == "/favicon.ico") {
    return;
  }
  // 处理url
  let myUrl = new URL(req.url, "http://127.0.0.1:3030/");
  // console.log(myUrl)

  // 处理静态文件
  postStatic(req, res);

  // 声明变量,存储 data.json 文件路径
  let file_path = path.join(__dirname, "public", "static", "data.json");
  if (myUrl.pathname == "/api/list") {
    /**
     * windows 系统中目录分隔符使用 '/' 或者 '\' 都行
     * linux 系统中目录分隔符使用 '/'
     * 所以不建议开发者自己写死目录分隔符
     * 另外,读取文件的时候推荐使用绝对路径(G:\2019级_online\20200122node\code)
     *
     */

    let json = fs.readFileSync(file_path);
    
    /**
     * 使用 Moment 包对日期和时间进行格式化,使用方法
     * 1)安装 moment :npm install moment
     * 2)引入 moment : const moment=require('moment)
     * 3)使用:moment(item.create_time).format('YYYY-MM-DD HH:mm:ss')
     */

    // 将数据转换成数组
    let json_data = JSON.parse(json);

    json_data.forEach((item) => {
        // 将时间戳转换为时间
      let result = moment(item.create_time).format("YYYY-MM-DD HH:mm:ss");
      item.create_time = result;
      // console.log(result);
    });
    // console.log(json_data);
    res.end(JSON.stringify(json_data));

    // 添加
  } else if (myUrl.pathname == "/api/add") {
    /**
     * 获取以 post 方式提交的数据,需要用到 data和end 两个事件
     * 在 data 事件中不断获取提交的数据,进行累加
     * 当客户端提交的数据全部获取后,会触发 end 事件
     */
    let postData = "";
    req.on("data", (params) => {
      postData += params;
    });
    req.on("end", () => {
      // 读取 data.json 文件
      let blogs = JSON.parse(fs.readFileSync(file_path, "utf8"));
      blogs.push(JSON.parse(postData));
      // JSON.stringify 转换成字符串
      fs.writeFileSync(file_path, JSON.stringify(blogs), "utf8");
      res.end("success");
    });
    // 编辑 获取你所要编辑的id
  } else if (myUrl.pathname == "/api/blog") {
      //URLsearchParams API提供对 URL 查询部分的读写权限
      //获取你所要修改的id的内容
    let id = myUrl.searchParams.get("id");
    //读取文件
    let blogs = JSON.parse(fs.readFileSync(file_path, "utf8"));

    // filter 筛选,筛选出来符合的数字
    let filter_data=blogs.filter((item) => {
      return item.id == id;
    });

    // 取出我们所需要的字符串
    // console.log(filter_data[0]);
    res.end(JSON.stringify(filter_data[0]))
    // 编辑 修改内容
  }else if(myUrl.pathname=='/api/edite'){
    let postData=''
    req.on('data',params=>{
      postData+=params
    })
    req.on('end',()=>{
      // 将 postData 转换成json对象
      let json=JSON.parse(postData)
      
      // 读取 data.json 文件
      let blogs = JSON.parse(fs.readFileSync(file_path, "utf8"));
      // 使用循环读取id
      blogs.forEach(item=>{
        if(item.id==json.id){
          // name 等于读取出来的 name
          item.name=json.name;
          item.age=json.age;
        }
      })
      fs.writeFileSync(file_path, JSON.stringify(blogs), "utf8");
      res.end('success')
    })
    // 删除
  }else if(myUrl.pathname=='/api/del'){
    let del_index = 0;
    let id=myUrl.search.split('=')[1]
    let blogs = JSON.parse(fs.readFileSync(file_path,"utf8"))
    let filter_data = blogs.filter((item,index,arr)=>{ // item 在这是数组每个元素
      if(item.id==id){
        del_index =index
      }
    })
    blogs.splice(del_index,1)
    fs.writeFileSync(file_path, JSON.stringify(blogs), "utf8");
    res.end('success')
  }
});


// 启动服务,并设置监听端口
app.listen("3030", () => {
  console.log("Server is running at http://127.0.0.1:3030");
});

function postStatic(req, res) {
  const mime_arr = [
    "image/jpeg",
    "image/png",
    "application/javascript",
    "text/css",
    "text/html",
  ];
  // 获取请求资源的 Mime 类型

  // 最终 myUrl 的值位 :http://127.0.0.1:3030/edite.html?id=1
  //req.url 相对路径
  req.url = req.url=='/'?'/index.html':req.url
  let myUrl = new URL(req.url, "http://127.0.0.1:3030/");
  let type = mime.getType(myUrl.pathname);

  if (mime_arr.indexOf(type) != -1) {
    let static_file_path = path.join(__dirname,"public","static",myUrl.pathname);
    let content = fs.readFileSync(static_file_path);
    res.end(content);
  }
}

在我们首页展示时,我们所创建的时间是时间戳的方式,说以我们需要将它转换成我们能看懂的格式

这时候我们需要安装一个模块 moment 模块

使用 Moment 包对日期和时间进行格式化,使用方法

1)安装 moment :npm install moment

2)引入 moment : const moment=require('moment)

3)使用:moment(item.create_time).format(‘YYYY-MM-DD HH:mm:ss’)

他们所请求的html文件

首页文件(index.html)

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Document</title>
  <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.5.3/css/bootstrap.css" rel="stylesheet" />
</head>

<body>
  <div class="container">
    <a href="/add.html" class="btn btn-success">Add</a>
    <table class="table table table-striped">
      <thead>
        <tr>
          <th>编号</th>
          <th>姓名</th>
          <th>年龄</th>
          <th>录入时间</th>
          <th>操作</th>
        </tr>
      </thead>
      <tbody>

      </tbody>
    </table>
  </div>
  <!-- <script src="/node_modules/moment/moment.js"></script> -->
  <script src="/http.js"></script>
  <script>
    http_get("/api/list", (data) => {
      let blogs = JSON.parse(data);
      blogs.forEach((item, index) => {
        // let format_time = moment(item.create_time,'YY-MM-dd')
        let tr = `
          <tr>
            <td>${item.id}</td>
            <td>${item.name}</td>
            <td>${item.age}</td>
            <td>${item.create_time}</td>
            <td>
              <a href="javascript:;" data-id=${item.id} class="edite btn btn-primary btn-sm">编辑</a>
              <a href="javascript:;" data-id=${item.id} class="delete btn btn-danger btn-sm">删除</a>
            </td>
          </tr>`;
        document.querySelector("tbody").insertAdjacentHTML("afterbegin", tr);
      });
    });
   // 事件委托的方式位编辑和删除添加单击事件
   let tbody=document.querySelector('tbody')
      tbody.addEventListener('click',event=>{
        // event.target 获取直接出发的单击事件元素
        let id=event.target.dataset.id
        if(event.target.innerHTML=='编辑'){
          window.location.href='edite.html?id='+id
        }else if(event.target.innerHTML=='删除'){
          window.location.href='del.html?id='+id
        }
      })
  </script>
</body>

</html>

增加页面代码(add.js)

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.5.3/css/bootstrap.css" rel="stylesheet" />
</head>

<body>
    <div class="container">
        <form>
            <div class="form-group">
                <label for="name">姓名</label>
                <input type="text" class="form-control" id="name" aria-describedby="emailHelp" />
                <label for="age">年龄</label>
                <input type="text" class="form-control" id="age" aria-describedby="emailHelp" />
            </div>
            <button type="button" class="btn btn-primary">Submit</button>
        </form>
        <script src="/http.js"></script>
        <script>
            document.querySelector(".btn").addEventListener("click", () => {
                let params = {
                    id: S4(),
                    name: document.querySelector("#name").value,
                    age: document.querySelector("#age").value,
                    create_time: Date.now(),
                };
                // 当请求成功时,进入ajax请求
                http_post("/api/add", params, (data) => {
                    if (data == "success") {
                        window.location.href = "/index.html";
                    }
                });
            });
            // 随机生成唯一的id
            function S4() {
                let stamp = new Date().getTime();
                return (((1 + Math.random()) * stamp) | 0).toString(16);
            }
        </script>
    </div>
</body>

</html>

更新页面代码(edite.html)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.5.3/css/bootstrap.css" rel="stylesheet" />
    <title>Document</title>
</head>
<body>
    <div class="container">
        <form>
            <div class="form-group">
                <label for="name">姓名</label>
                <input type="text" class="form-control" id="name" aria-describedby="emailHelp" />
                <label for="age">年龄</label>
                <input type="text" class="form-control" id="age" aria-describedby="emailHelp" />
            </div>
            <button type="button" class="btn btn-primary">Submit</button>
        </form>
        <script src="/http.js"></script>
        <script>
            // 获取 传递过来的博客 id 的值
            let id=window.location.search.split('=')[1];
            console.log(id)
            http_get("/api/blog?id=" + id,(data)=>{
                let json = JSON.parse(data)
                document.querySelector('#name').value=json.name;
                document.querySelector('#age').value=json.age;
            })


            document.querySelector(".btn").addEventListener("click", () => {
                let params = {
                    // 前面的id是名称,后面的id是我们所传递过来的id
                    id: id,
                    name: document.querySelector("#name").value,
                    age: document.querySelector("#age").value,
                };
                // 当请求成功时,进入ajax请求
                http_post("/api/edite", params, (data) => {
                    if (data == "success") {
                        window.location.href = "index.html";
                    }
                });
            });
        </script>
    </div>
</body>
</html>

删除页面(del.html)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <!-- <h1>删除成功</h1> -->
    <script src="/http.js"></script>
    <script>
        window.onload=function(){
            if(confirm('确定要删除?')){
                //获取id
                let id=window.location.search.split('=')[1];
                http_get('/api/del?id='+id,(data)=>{
                    if(data=="success"){
                        window.location.href='index.html'
                    }
                })
            }
        }
    </script>
</body>
</html>

由于他们请求的方式不同,所以他们所经过的ajax方式也不同

首页展示,使用get方式,而更新和添加都是表单提交

他们的ajax代码请求如下:

function http_get(url,callback){
    let xhr=new XMLHttpRequest()
    xhr.open('get',url)
    xhr.onreadystatechange=()=>{
        if(xhr.readyState==4 && xhr.status==200){
            callback && callback(xhr.responseText)
        }
    }
    xhr.send()
}
function http_post(url,params,callback){
    let xhr=new XMLHttpRequest()
    xhr.open('post',url)
    xhr.onreadystatechange=()=>{
        if(xhr.readyState==4 && xhr.status==200){
            callback && callback(xhr.responseText)
        }
    }
    xhr.setRequestHeader('Content-type','application/x-www-form-urlencoded')
    xhr.send(JSON.stringify(params))
}

当我们做好了客户端的开发,为了从服务器获取数据,就要从后台调用接口进行json解析。

由于没有数据库,但还是要测试的时候,就需要假数据来实现这部分的功能。

data.json文件

[
    {
        "id": "4dee955",
        "title": "今天雪好大啊",
        "create_time": 1611542307364
    },
    {
        "id": "ae8b9a4",
        "title": "bbb",
        "create_time": 1611542310184
    }
]

运行结果

任务单.gif

动态资源

相同的请求地址不同的响应资源,这种资源就是动态资源。

就像我们的删除单条数据一样,前面ip一样,只不过所对应的数据不一样

image.png

image.png

Node.js 异步编程

同步API和异步API的区别

返回值

同步API可以从返回值中拿到API执行的结果, 但是异步API是不可以的

// 同步
function sum (n1,n2){
    return n1 + n2
}
const result = sum (10,20);
console.log(result);
  
// 异步
function sum (n1,n2){
    setTimeout(
        ()=>{
            return n1 + n2;
    },2000);
    // return 1
}
const result = sum (10,20);
console.log(result);

执行顺序

同步API:只有当前API执行完成后,才能继续执行下一个API

异步API:当前API的执行不会阻塞后续代码的执行

记住,他们的名字,和他们的执行循序相反,同步,一个一个执行,异步一起执行

代码执行顺序分析
  • 按照顺序从上到下执行代码
  • 遇到同步代码,直接执行
  • 遇到异步代码,放到异步代码执行区
  • 所有的同步代码执行完毕后,再开始按照顺序执行异步代码知行区中的代码
  • 执行完毕后,将回调函数放到回调函数队列(也在同步代码执行区域),然后执行
  • 因为异步任务的耗时不一样,所以可能排在前面的异步代码,反而最后才执行完,所以其回调函数可能最后才执行

img

回调函数

回调函数是常用的解决异步的方法,经常接触和使用到,易于理解,并且在库或函数中非常容易实现。这种也是大家接使用异步编程经常使用到的方法。

但是回调函数的方式存在如下的问题:

  1. 可能形成万恶的嵌套金字塔,代码不易阅读;

  2. 只能对应一个回调函数,在很多场景中成为一个限制。

  3. 异常的处理

    node通常会将异常作为回调函数的第一个实参传回,如果第一个参数为null,那么就说明异步调用没有异常抛出。

自己定义函数让别人去调用。

  // getData函数定义
 function getData (callback) {}
  // getData函数调用
 getData (() => {});

异步的执行结果只能通过回调函数获取并处理

Node.js中的异步API

文件的读取

引入 fs 模块

const fs = require("fs");

readFileSync 方法是一个 同步函数

let a = fs.readFileSync("a.txt","utf8");
console.log(a)
let b = fs.readFileSync("b.txt","utf8");
console.log(b)
let c = fs.readFileSync("c.txt","utf8");
console.log(c)
let d = fs.readFileSync("d.txt","utf8");
console.log(d)

image.png

readFile 方法是一个 异步方法,读取的内容不是通过返回值

而是,需要提供一个回调函数,会将读取的内容赋值给 data 变量

所有只有在回调函数中,才可以获取读取的内容

顺序是随机的,所以我们要通过函数的嵌套实现按照顺序异步回调函数的目标

fs.readFile("a.txt","utf8",function(err,data){
    console.log(data);
    fs.readFile("b.txt","utf8",function(err,data){
        console.log(data);
        fs.readFile("c.txt","utf8",function(err,data){
            console.log(data);
            fs.readFile("d.txt","utf8",function(err,data){
                console.log(data);
            })
        })
    })
})

image.png

回调地狱

javascript的回调虽然非常的优秀,它有效的解决了同步处理的问题。但是遗憾的是,如果我们需要依赖回调函数的返回值来进行下一步的操作的时候,就会陷入这个回调地狱。

叫回调地狱有点夸张了,但是也是从一方面反映了回调函数所存在的问题。就例如上面的代码,他就是有无数个嵌套组成,会让人一眼看上去不知所云,晕的不行。

对于此种情况,我们优秀的程序员,研发了各种方法

Promise

Promise出现的目的是解决Node.js异步编程中回调地狱的问题。

什么是Promise

Promise 是异步编程的一种解决方案,比传统的解决方案“回调函数和事件”更合理和更强大。

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。

从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。

Promise的特点

Promise有两个特点:

  1. 对象的状态不受外界影响。

Promise对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved(已完成,又称 Fulfilled)和Rejected(已失败)。

只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。

  1. 一旦状态改变,就不会再变,任何时候都可以得到这个结果。

Promise对象的状态改变,只有两种可能:从Pending变为Resolved和从Pending变为Rejected。

这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

Promise的优点

Promise将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。

Promise对象提供统一的接口,使得控制异步操作更加容易。

Promise的缺点

  1. 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
  2. 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
  3. 当处于Pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

所以我们上面的代码则可以改为Promise方法

const fs = require('fs')
// resolve 和 reject 就是两个形参,表示两个回调函数,开发者根据代码的执行结果
// 成功了,调用resolve函数,失败了调用reject函数,同时传递参数
// 主要是用来解决 回调函数的嵌套问题
function p1(){
    return new Promise((resolve, reject) => {
        // 将异步代码写到这个函数中
        fs.readFile('a.txt', 'utf8', (err, data) => {
            if (err) {
                reject('读取失败了')
            } else {
                resolve(data)
            }
        })
    })
}
function p2(){
    return new Promise((resolve, reject) => {
        // 将异步代码写到这个函数中
        fs.readFile('b.txt', 'utf8', (err, data) => {
            if (err) {
                reject('读取失败了')
            } else {
                resolve(data)
            }
        })
    })
}
function p3(){
    return new Promise((resolve, reject) => {
        // 将异步代码写到这个函数中
        fs.readFile('c.txt', 'utf8', (err, data) => {
            if (err) {
                reject('读取失败了')
            } else {
                resolve(data)
            }
        })
    })
}
function p4(){
    return new Promise((resolve, reject) => {
        // 将异步代码写到这个函数中
        fs.readFile('d.txt', 'utf8', (err, data) => {
            if (err) {
                reject('读取失败了')
            } else {
                resolve(data)
            }
        })
    })
}
/*异步任务结束后,
如果成功了,会触发then事件,我们就传递一个实参进去,这个参数是一个函数,会赋值给上面的形参
resolve
如果失败了,会触发catch事件,我们就传递一个实参进去,这个参数是一个函数,会赋值给上面的形参
reject
Promise一经创建就会立马执行。但是Promise.then中的方法,则会等到一个调用周期过后再次调用
*/
p1().then(result => {
    console.log(result)
    return p2()
}).then(result=>{
    console.log(result)
    return p3()
}).then(result=>{
    console.log(result)
    return p4()
}).then(result=>{
    console.log(result)
}).catch(err => {
    console.log(err)
})

image.png

异步函数

Promise当然很好,我们将回调地狱转换成了链式调用。我们用then来将多个Promise连接起来,前一个promise resolve的结果是下一个promise中then的参数。

链式调用有什么缺点呢?

比如我们从一个promise中,resolve了一个值,我们需要根据这个值来进行一些业务逻辑的处理。

假如这个业务逻辑很长,我们就需要在下一个then中写很长的业务逻辑代码。这样让我们的代码看起来非常的冗余。

那么有没有什么办法可以直接返回promise中resolve的结果呢?

答案就是await。

await 关键字

  • await关键字只能出现在异步函数中
  • await promise await后面只能写promise对象 写其他类型的API是不不可以的
  • await关键字可是暂停异步函数向下执行 直到promise返回结果

当promise前面加上await的时候,调用的代码就会停止直到 promise 被解决或被拒绝。

注意await一定要放在async函数中,例如:

const logAsync = () => {
  return new Promise(resolve => {
    setTimeout(() => resolve('123'), 5000)
  })
}

上面我们定义了一个logAsync函数,该函数返回一个Promise,因为该Promise内部使用了setTimeout来resolve,所以我们可以将其看成是异步的。

要是使用await得到resolve的值,我们需要将其放在一个async的函数中:

const doSomething = async () => {
  const resolveValue = await logAsync();
  console.log(resolveValue);
}

async的执行顺序

await实际上是去等待promise的resolve结果我们把上面的例子结合起来

aysnc是异步执行的,并且它的顺序是在当前这个周期之后

async的特点

  • async会让所有后面接的函数都变成Promise,即使后面的函数没有显示的返回Promise。

  • 因为只有Promise才能在后面接then,我们可以看出async将一个普通的函数封装成了一个Promise

  • 普通函数定义前加async关键字 普通函数变成异步函数

  • 异步函数默认返回promise对象

  • 在异步函数内部使用return关键字进行结果返回 结果会被包裹的promise对象中 return关键字代替了resolve方法

  • 在异步函数内部使用throw关键字抛出程序异常

  • 调用异步函数再链式调用then方法获取异步函数执行结果

  • 调用异步函数再链式调用catch方法获取异步函数执行的错误信息


版权声明:本文为zhzijun原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。