1. 服务器端基础概念
1.1网站组成
- 网站应用程序主要分为两大部分:客户端和服务器端。
- 客户端:在浏览器中运行的部分,就是用户看到并与之交互的界面程序。使用HTML、CSS、JavaScript构建。
- 服务器端:在服务器中运行的部分,负责存储数据和处理应用逻辑。
1.2Node网站服务器
能够提供网站访问服务的机器就是网站服务器,它能够接收客户端的请求(request),能够对请求做出响应(respond)。
1.3 IP地址、域名和端口
IP地址:是指互联网中设备的唯一标识。这里不多叙述,大家应该都了解一些~
域名:是指平时上网所使用的网址。如:www.baidu.com,虽然在地址栏中输入的是网址, 但是最终还是会将域名转换为ip才能访问到指定的网站服务器。
例如:http://www.baidu.com => http://220.181.38.148/
端口:是指计算机与外界通讯交流的出口,用来区分服务器电脑中提供的不同的服务。
因为大多数网站应用使用的是80端口号,如果输入域名或IP访问网站时没有输入端口号,浏览器在请求的时候默认在输入的域名或IP地址后面接的是80端口号,如果是非80端口号,在IP地址或域名后面加上端口号即可。
1.4 URL
统一资源定位符,又叫URL(Uniform Resource Locator),是专为标识Internet网上资源位置而设的一种编址方式,我们平时所说的网页地址指的即是URL。在URL中标注了要请求的服务器地址,提供服务的端口及要请求的资源位置。
URL的组成
传输协议://服务器IP或域名:端口/资源所在位置标识
https://editor.csdn.net/md?articleId=106287460
http:超文本传输协议,提供了一种发布和接收HTML页面的方法。
1.5开发过程中客户端和服务器端说明
在开发阶段,客户端和服务器端使用同一台电脑,即开发人员电脑。
我们自己电脑的服务器(本地)可以通过一组特殊的IP或域名来访问
本机域名:localhost
本机IP地址:127.0.0.1
2. 创建web服务器
我们使用nodejs当中创建网站服务器。做服务器端开发如果没有网站服务器,一切都无从谈起。
在电脑当中,我们要先安装好node软件,参考node学习笔记,再用nodejs创建软件层面上的服务器,得到请求对象和响应对象。
创建网站服务器实例代码如下图:
//引入http创建网站服务器系统模块
const http = require('http');
//创建网站服务器对象
const app = http.createServer();
//当客户端有请求来的时候
app.on('request',(req,res) => {
//响应
res.end('hello');
});
//监听的端口号
app.listen(3000);
console.log('网站服务器启动成功');
保存代码,打开PowerShell窗口,切换到当前目录,使用nodemon xxx.js(当前文件名),这里也可以用node命令,但是建议用nodemon,这样每次修改保存文件后可以自动运行,无需手动再去运行一遍
nodemon test.js
打开浏览器,在网址栏输入localhost:3000,测试成功,响应内容成功显示出来。(如果你响应的内容是中文字符,此时代码还不够完善所以显示出来的不是想要的效果,这个问题后面会提到)
3. HTTP协议
3.1 HTTP协议的概念
超文本传输协议(英文:HyperText Transfer Protocol,缩写:HTTP)规定了如何从网站服务器传输超文本到本地浏览器,它基于客户端服务器架构工作,是客户端(用户)和服务器端(网站)请求和应答的标准。
3.2 报文
在HTTP请求和响应的过程中传递的数据块就叫报文,包括要传送的数据和一些附加信息,并且要遵守规定好的格式。
拿博客首页来看看
信息量挺大的,可以了解一些
3.3 请求报文
请求方式 (Request Method)
- GET 请求数据
- POST 发送数据
最常见的get请求就是浏览器通过地址栏中输入的网址的方式。如果既不是获取数据也不是添加数据的请求,比如登陆操作,一般是post方式,因为使用post相对于get 更安全一些。
实例代码如下图,保存文件,
//引入http,创建网站服务器模块
const http = require('http');
//app对象就是网站服务器对象
const app = http.createServer();
//当客户端有请求来的时候
app.on('request',(req,res) => {
//获取请求方式
// console.log(req.method);
if(req.method=='POST'){
res.end('post');
}else if(req.method=='GET'){
res.end('get');
}else {}
});
//监听的端口号
app.listen(3000);
console.log('网站服务器启动成功');
编写一个表单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>
<!-- method:指定当前表单的提交方式
action:指定当前表单提交的地址 -->
<form method="POST" action="http://localhost:3000/">
<input type="submit" name="提交">
</form>
</body>
</html>
保存后运行,点击提交按钮,可以看到此时的请求方式就是post。
请求地址 (Request URL)
app.on('request', (req, res) => {
req.headers // 获取请求报文
req.url // 获取请求地址
req.method // 获取请求方法
});
3.4 响应报文
HTTP状态码
- 200 请求成功
- 404 请求的资源没有被找到
- 500 服务器端错误
- 400 客户端请求有语法错误
内容类型
- text/html
- text/css
- application/javascript
- image/jpeg
- application/json
app.on('request', (req, res) => {
// 设置响应报文
res.writeHead(200, {
'Content-Type': 'text/html;charset=utf8‘
});
});
综合请求地址及响应报文应用实例代码如下图:
//引入http创建网站服务器系统模块
const http = require('http');
//创建网站服务器对象
const app = http.createServer();
//当客户端有请求来的时候
app.on('request',(req,res) => {
//获取请求地址
// res.end(req.url);
//响应报文
res.writeHead(200,{
//设置了响应报文的文本类型和字符编码格式后,中文以及html标签的格式就可以在浏览器渲染出来了
'content-type':'text/html;charset=utf8'
});
if(req.url=='/index' || req.url=='/'){
res.end('<h2>这是index界面</h2>');
}else if(req.url=='/list'){
res.end('<h2>这是list界面</h2>');
}else {
res.end('<h3>404</h3>');
}
});
//监听的端口号
app.listen(3000);
console.log('网站服务器启动成功');
4. HTTP请求与响应处理
4.1 请求参数
客户端向服务器端发送请求时,有时需要携带一些客户信息,客户信息需要通过请求参数的形式传递到服务器端,比如登录操作。
4.2 GET请求参数
参数被放置在浏览器地址栏中,例如:http://localhost:3000/?name=zhangsan&age=20
参数获取需要借助系统模块url,url模块用来处理url地址
const http = require('http');
// 导入url系统模块 用于处理url地址
const url = require('url');
const app = http.createServer();
app.on('request', (req, res) => {
//响应报文
res.writeHead(200,{
'content-type':'text/html;charset=utf8'
});
//获取请求地址
// console.log(req.url);
// 将url路径的各个部分解析出来并返回对象
//获取url相应对象方式,true 代表将参数解析为对象格式
// let query = url.parse(req.url).query;
// let paras = url.parse(req.url).query;
// console.log(paras);
// console.log(paras.name);
// console.log(paras.age);
// let {query} = url.parse(req.url,true);
//let {pathname,query} = url.parse(req.url,true);
let {pathname,query} = url.parse(req.url,true);
if(pathname=='/index' || pathname=='/'){
res.end('<h2>这是index界面</h2>');
console.log(query);
}else if(pathname=='/list'){
res.end('<h2>这是list界面</h2>');
console.log(query);
}else {
res.end('<h3>404</h3>');
}
});
app.listen(3000);
在浏览器地址栏输入:http://localhost:3000/index?name=zahngsan&age=18
4.3 POST请求参数
- 参数被放置在请求体中进行传输
- 获取POST参数需要使用data事件和end事件
- 使用querystring系统模块将参数转换为对象格式
// 导入系统模块querystring 用于将HTTP参数转换为对象格式
const querystring = require('querystring');
app.on('request', (req, res) => {
let postData = '';
// 监听参数传输事件
req.on('data', (chunk) => postData += chunk;);
// 监听参数传输完毕事件
req.on('end', () => {
console.log(querystring.parse(postData));
});
res.end('hello');
});
通过表单文件点击按钮形式请求参数
4.4 路由
http://localhost:3000/index
http://localhost:3000/login
路由是指客户端请求地址与服务器端程序代码的对应关系。简单的说,就是请求什么响应什么。
实例代码:
// 当客户端发来请求的时候
app.on('request', (req, res) => {
//响应报文,设置文本类型和字符编码
res.writeHead(200,{
'content-type':'text/html;charset=utf8'
});
//获取请求方式
let method = req.method.toLowerCase();
//获取请求地址的pathname对象和query参数对象
let {pathname,query} = url.parse(req.url,true);
if(method == 'get'){
if(pathname=='/index' || pathname=='/'){
res.end('<h2>这是index界面</h2>');
console.log(query);
}else if(pathname=='/list'){
res.end('<h2>这是list界面</h2>');
console.log(query);
}else {
res.end('<h3>404</h3>');
}
}else if(method == 'post'){
}else {}
});
4.5 静态资源和动态资源
静态资源:服务器端不需要处理,可以直接响应给客户端的资源就是静态资源,例如CSS、JavaScript、image文件。如:http://www.itcast.cn/images/logo.png
动态资源:相同的请求地址不同的响应资源,这种资源就是动态资源。
如:http://www.itcast.cn/article?id=1
http://www.itcast.cn/article?id=2
//创建网站服务器模块
const http = require('http');
//请求地址模块
const url = require('url');
//路径模块
const path = require('path');
//文件模块
const fs = require('fs');
//mime模块,引用前需要下载mime模块
const mime = require('mime');
//创建服务器对象
const app = http.createServer();
//请求响应服务器对象
app.on('request',(req,res) => {
//获取请求路径
let pathname = url.parse(req.url).pathname;
pathname = pathname == '/'? '/default.html' : pathname;
//转行成物理地址
let realPath = path.join(__dirname,'public'+pathname);
//获取文件类型
let type = mime.getType(realPath);
//读取文件
fs.readFile(realPath,(err,result) => {
if(err != null){
res.writeHead(404,{'content-type':'text/html;charset=utf8'});
res.end('文件读取失败!');
return ;
}
//响应报文
res.writeHead(200,{'content-type':type});
res.end(result);
});
});
//监听端口号
app.listen(3000);
console.log("网站服务器启动成功");
下载mime模块命令
npm install mime
4.6 客户端请求途径
GET方式
- 浏览器地址栏
- link标签的 href属性
- script标签的src属性
- img标签的src属性
- Form表单提交
POST方式
- Form表单提交
5.Node.js异步编程
5.1 同步API, 异步API
同步API:只有当前API执行完成后,才能继续执行下一个API
console.log('before');
console.log('after');
异步API:当前API的执行不会阻塞后续代码的执行
//输出顺序:before after last
console.log('before');
setTimeout(
() => { console.log('last');
}, 2000);
console.log('after');
5.2 同步API, 异步API的区别( 获取返回值 )
同步API可以从返回值中拿到API执行的结果, 但是异步API是不可以的
// 同步
function sum (n1, n2) {
return n1 + n2;
}
const result = sum (10, 20);
// 异步,下面代码
function getMsg () {
setTimeout(function () {
return { msg: 'Hello Node.js' }
}, 2000);
}
//结果undefined
console.log(getMsg());
5.3 回调函数
自己定义函数让别人去调用。
function getData (callback) {
callback('123')
}
getData(function (n) {
console.log('callback函数被调用了')
console.log(n)
});
5.4 使用回调函数获取异步API执行结果
function getMsg (callback) {
setTimeout(function () {
callback ({ msg: 'Hello Node.js' })
}, 2000);
}
getMsg (function (msg) {
console.log(msg);
});
5.5 同步API, 异步API的区别(代码执行顺序)
同步API从上到下依次执行,前面代码会阻塞后面代码的执行
for (var i = 0; i < 100000; i++) {
console.log(i);
}
console.log('for循环后面的代码');
异步API不会等待API执行完成后再向下执行代码
console.log('代码开始执行');
setTimeout(() => { console.log('2秒后执行的代码')}, 2000);
setTimeout(() => { console.log('0秒后执行的代码')}, 0);
console.log('代码结束执行');
5.6 代码执行顺序分析
//下列代码执行结果
//代码开始执行
//代码结束执行
//0秒后执行的代码
//2秒后执行的代码
console.log('代码开始执行');
setTimeout(() => {
console.log('2秒后执行的代码');
}, 2000);
setTimeout(() => {
console.log('0秒后执行的代码');
}, 0);
console.log('代码结束执行');
5.7 Node.js中的异步API
前面学过的读取文件和事件监听属于常见的异步API
//读取文件
fs.readFile('./demo.txt', (err, result) => {});
//事件监听
var server = http.createServer();
server.on('request', (req, res) => {});
如果异步API后面代码的执行依赖当前异步API的执行结果,但实际上后续代码在执行的时候异步API还没有返回结果,这个问题要怎么解决呢?
fs.readFile('./demo.txt', (err, result) => {});
console.log('文件读取结果');
需求:依次读取A文件、B文件、C文件
方法一:套娃方式,倘若读取的文件n个,显然用这种方法需要“套娃”n-1遍,造成回调地狱。这种方法显然不适合用来读取过多的文件。
const fs = require('fs');
fs.readFile('./1.txt', 'utf8', (err, result1) => {
console.log(result1)
fs.readFile('./2.txt', 'utf8', (err, result2) => {
console.log(result2)
fs.readFile('./3.txt', 'utf8', (err, result3) => {
console.log(result3)
})
})
});
5.8 Promise
方法二:使用Promise,Promise出现的目的是解决Node.js异步编程中回调地狱的问题。
//resolve执行成功的返回结果,结果以对象形式存储
//reject执行失败的返回结果
//resolve和reject本质是函数
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
if (true) {
resolve({name: '张三'})
}else {
reject('失败了')
}
}, 2000);
});
promise.then(result => console.log(result); // {name: '张三'})
.catch(error => console.log(error); // 失败了);
利用Promise方式读取单个文件
const fs = require('fs');
let promise = new Promise((resolve, reject) => {
fs.readFile('./100.txt', 'utf8', (err, result) => {
if (err != null) {//读取文件失败
reject(err);
}else {//成功读取文件
//用Promise执行成功的返回函数resolve返回结果
resolve(result);
}
});
});
//用then接收成功的返回结果
promise.then((result) => {
console.log(result);
})
//catch捕捉执行异常
.catch((err)=> {
console.log(err);
})
利用Promise方式读取多个文件
const fs = require('fs');
function p1() {
return new Promise((resolve,reject) => {
fs.readFile('1.txt','utf8',(err,result) => {
if(err != null){
reject(err);
}else {
resolve(result);
}
});
});
}
function p2() {
return new Promise((resolve,reject) => {
fs.readFile('2.txt','utf8',(err,result) => {
if(err != null){
reject(err);
}else {
resolve(result);
}
});
});
}
function p3() {
return new Promise((resolve,reject) => {
fs.readFile('3.txt','utf8',(err,result) => {
if(err != null){
reject(err);
}else {
resolve(result);
}
});
});
}
p1().then((r1) => {
console.log(r1);
return p2();
})
.then((r2) => {
console.log(r2);
return p3();
})
.then((r3) => {
console.log(r3);
})
虽然这个方式解决了回调地狱问题,但是代码过于繁琐,怪吓人的~
5.9 异步函数
方法三:使用async关键字
异步函数是异步编程语法的终极解决方案,它可以让我们将异步代码写成同步的形式,让代码不再有回调函数嵌套,使代码变得清晰明了。
const fn = async () => {};
async function fn () {};
//可以测试一下,使用async关键字的函数,其返回结果为promise对象
async function fn () {
throw '发生了一些错误';
return 123;
}
// console.log(fn ())
fn ().then(function (data) {
console.log(data);
}).catch(function (err){
console.log(err);
})
async关键字
- 普通函数定义前加async关键字 普通函数变成异步函数
- 异步函数默认返回promise对象
- 在异步函数内部使用return关键字进行结果返回 结果会被包裹的promise对象中 return关键字代替了resolve方法
- 在异步函数内部使用throw关键字抛出程序异常
- 调用异步函数再链式调用then方法获取异步函数执行结果
- 调用异步函数再链式调用catch方法获取异步函数执行的错误信息
await关键字
- await关键字只能出现在异步函数中
- await promise await后面只能写promise对象 写其他类型的API是不不可以的
- await关键字可是暂停异步函数向下执行 直到promise返回结果
使用async关键字函数,读取多个文件,解决了上述的回调地狱问题以及代繁琐问题,从而使代码简洁易懂。
const fs = require('fs');
//改造现有的异步API,让其返回promise对象,从而支持异步函数语法
const readFile = require('util').promisify(fs.readFile);
async function run() {
let f1 = await readFile('1.txt','utf8');
let f2 = await readFile('2.txt','utf8');
let f3 = await readFile('3.txt','utf8');
console.log(f1);
console.log(f2);
console.log(f3);
}
run();
util模块
const fs = require('fs');
//获取util模块下的promisify方法,
const promisify = require('util').promisify
//promisify方法可以改造异步API,让其返回promise对象
const readFile = promisify(fs.readFile);