提示
具体内容查看 Node 的 API 文档 (opens new window)
无论是前面的学习,还是官网文档中的案例(同时演示了 CJS/ESM 规范两种写法),都表明 Node 也在跟随着时代发展的脚步
# 准备工作
为了简化手动重复的重新启动 http 服务,我们可以使用 nodemon 辅助包,来监听变化并自动重启 http 服务。
# 安装
npm i nodemon -g
# 使用:
nodemon app.js
另外,如果不想被浏览器影响到,我们在可以使用 apifox、postman 等工具进行接口测试。
# 一、http 服务
Content-Type 值
res.writeHead(200, { 'Content-Type': 'text/html;charset=utf-8' })
- 表单
application/x-www-form-urlencoded;
- json 数据
application/json;
- 纯文本
text/plain
- html 数据
text/html
- 上传文件
multipart/form-data
http
├─ Class: http.Server
│ ├─ Event: 'request'
│ ├─ server.listen()
| └─ ……
├─ Class: http.ServerResponse
│ ├─ response.writeHead()
│ ├─ response.write()
│ ├─ response.end()
| └─ ……
├─ Class: http.IncomingMessage
│ ├─ message.headers
│ ├─ message.method
│ ├─ message.url
| └─ ……
└─ http.createServer
# 1、createServer
http.createServer([options][, requestListener])
http 模块 与 Stream 流
其实,http.createServer
和前面提到的 fs.createReadStream
、fs.createWriteStream
一样,都是常见流 Readable (opens new window)、Writable (opens new window) 的应用之一。
http.createServer
:返回的是一个 http.Server (opens new window) 实例
requestListener 会自动添加到
http.Server
中的'request'事件中
const { createServer } = require('http')
const server = createServer((req, res) => {
//
res.end()
})
server.listen(6061)
# 2、server 实例
在 http.Server 中,我们常常使用下面两个事件/方法:
- request 事件
- listen 方法
const { createServer } = require('http')
const server = createServer()
let count = 0
// 监听HTTP的请求响应
server.on('request', (req, res) => {
count++
console.log('客户端发起了第' + count + '个请求')
console.log('请求地址为:' + req.url) // 浏览器中:"/" 和 "/favicon.ico"
res.end()
})
// 推荐端口范围 1025~65535
server.listen(6061, '0.0.0.0', () => {
console.log('端口为6061的服务器开启成功:http://localhost:6061')
})
# 3、请求处理 ✨
客户端发送的信息,我们可以通过 http.IncomingMessage
获取,并根据实际需求进行处理。
提示
在真实的开发框架中(如:express、koa 等),是不需要我们对请求类型、url 判断、参数处理等操作的,一般框架都会进行封装,例如:app.get('/problems/')
、req.body
还有一点就是,IncomingMessage (opens new window) 本身继承自 <stream.Readable> (opens new window);所以前者是可以使用后者的流读取事件和方法的。
const { createServer } = require('http')
const { parse } = require('url')
const server = createServer()
server.on('request', (req, res) => {
const url = req.url
const method = req.method
// const token = req.headers['authorization']
if (method === 'GET') {
// 1、query数据(在url路径中)
const { pathname, query } = parse(url, true)
switch (url) {
case '/problems':
//
break
default:
break
}
}
if (method === 'POST') {
// req.setEncoding('utf-8')
// 2、body数据(在stream流中)
let data = ''
req.on('data', (chunk) => {
data += chunk // += 内部会默认调用 toString方法(默认采用 utf-8)
})
req.on('end', () => {
console.log(JSON.parse(data)) // { username: '张三', password: 123456 }
})
switch (url) {
case '/login':
//
break
default:
break
}
}
res.end()
})
server.listen(6061)
# 2、响应处理 ✨
可以通过 http.ServerResponse
对客户端的请求做出响应。
TIP
res.writeHead 必须写在 res.write 的前面,不然会报错 😭
常见的状态码
常见 HTTP 状态码 | 状态描述 | 信息说明 |
---|---|---|
200 | OK | 客户端请求成功 |
201 | Created | POST 请求,创建新的资源 |
301 | Moved Permanently | 请求资源的 URL 已经修改,响应中会给出新的 URL |
400 | Bad Request | 客户端的错误,服务器无法或者不进行处理 |
401 | Unauthorized | 未授权的错误,必须携带请求的身份信息 |
403 | Forbidden | 客户端没有权限访问,被拒接 |
404 | Not Found | 服务器找不到请求的资源 |
500 | Internal Server Error | 服务器遇到了不知道如何处理的情况 |
503 | Service Unavailable | 服务器不可用,可能处理维护或者重载状态,暂时无法访问 |
const { createServer } = require('http')
const server = createServer()
server.on('request', (req, res) => {
// 状态码 与 响应头
res.statusCode = 403
res.setHeader('Content-Type', 'multipart/form-data;charset=utf-8')
// res.writeHead(403, { 'Content-Type': 'multipart/form-data;charset=utf-8' })
// 1、返回文本
res.writeHead(200, { 'Content-Type': 'text/html;charset=utf-8' })
res.write(`
<html>
<b>你好呀! Node.js</b>
</html>
`)
res.end()
// 2、返回接口
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(
JSON.stringify({
code: 1,
data: {
user: 'admin',
pwd: 123456
}
})
)
})
server.listen(6061)
# 二、http 代理
http 模块除了可以作为服务端处理请求,还可以作用客户端向第三方服务器发送请求。
并且,由于 Node 在服务器中,所以 http 模块发起的请求是不存在跨域问题的。
http
├─ http.createServer()
├─ http.get()
├─ http.request()
└─ ……
提示
注意的是,http.get()
是 http.request()
简化版本而言,并且它们的 option 参数内容是一致的。因此个人习惯于所有类型的请求都使用 http.request()
# 1、post 请求
const { request } = require('node:http')
// 请求数据
const postData = JSON.stringify({
title: 'foo',
body: 'bar',
userId: 1
})
// 请求选项
const option = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
}
// 1、发起post请求
const req = request('http://jsonplaceholder.typicode.com/posts', option, (res) => {
res.setEncoding('utf8')
res.on('data', (chunk) => {
const data = chunk.toString() // 响应结果
console.log(JSON.parse(data))
//
})
res.on('end', () => {
//
})
})
// 2、追加body数据
req.write(postData)
req.on('error', (err) => {
//
})
req.end()
# 2、get 请求
const { request } = require('node:http')
// 发起get请求
const req = request('http://jsonplaceholder.typicode.com/posts/1', { method: 'GET' }, (res) => {
// res.setEncoding('utf8')
res.on('data', (chunk) => {
const data = chunk.toString() // 响应结果
console.log(JSON.parse(data))
//
})
res.on('end', () => {
//
})
})
req.on('error', (err) => {
//
})
req.end()
# 3、axios 库
Axios 是一个基于 promise 网络请求库,作用于 node.js 和浏览器中
在服务端它使用原生 node.js
http 模块
, 而在客户端 (浏览端) 则使用XMLHttpRequests
。
自己可以对比一下使用原生 http 模块 和 使用 axios 之间的区别。
const axios = require('axios')
// 1、post 请求
axios
.post('http://jsonplaceholder.typicode.com/posts', {
title: 'foo',
body: 'bar',
userId: 1
})
.then((res) => {
console.log(res.data)
})
// 2、get 请求
axios.get('http://jsonplaceholder.typicode.com/posts/1').then((res) => {
console.log(res.data)
})
# 4、cheerio 包
爬虫模型:
有时前端可能不会要所有的数据,我们后端当然也可以直接拿取网页爬取数据,然后按需为前端提供数据。
npm i cheerio -S
官方:
cheerio (opens new window)是一款非常实用的 nodejs 第三方包,适用于服务端(nodejs 端)处理 html。它有着与 jquery 及其相似(几乎是一致)的 api,速度飞快,使用灵活,而且不仅能够处理 html,同样也能处理 xml。
需求:
获取电影列表的电影名、评分、主演组成的 json 数据。
代码编写
① 前端
<body>
<script>
fetch('http://127.0.0.1:3000/api/list')
.then((res) => res.json())
.then((res) => {
console.log(res)
})
</script>
</body>
② 后端
const http = require('http')
const url = require('url')
const https = require('https')
const cheerio = require('cheerio')
// 创建web服务器
http
.createServer((req, res) => {
const urlobj = url.parse(req.url, true)
callbackName = urlobj.query.callback
res.writeHead(200, {
'content-type': 'application',
// 跨域问题解决方案:CORS头
'Access-Control-Allow-Origin': '*'
})
switch (urlobj.pathname) {
case '/api/list':
http_get((data) => {
// 对网页数据进行过滤
res.end(spider(data))
})
break
default:
res.end('404')
break
}
res.end()
})
.listen(3000, () => {
console.log('Server running at http://localhost:3000')
})
function http_get(cb) {
var data = ''
// 使用node✨发起get请求(帮助前端跨域获取“猫眼”数据)
https.get(`https://i.maoyan.com/`, (res) => {
res.on('data', (chunk) => {
data += chunk
})
res.on('end', () => {
// response.end(data)
cb(data)
})
})
}
function spider(data) {
let $ = cheerio.load(data)
let $moviewlist = $('.column.content')
let movies = []
$moviewlist.each((index, value) => {
movies.push({
title: $(value).find('.title').text(),
grade: $(value).find('.grade').text(),
actor: $(value).find('.actor').text()
})
})
return JSON.stringify(movies)
}
# 三、代理服务器
前面我们已经知道了 Node 服务器既可以作为服务端的形式存在,也可以作为客户端的形式存在。
所以在服务端中,node 其实更多的是扮演 '房屋中介'(中间层) 的角色。
提示
类似的 web 服务器还有:Nginx、Apache Tomcat 等
# 1、代理分析
之所以 node 服务器可以作为代理商,是因为:
答案
- 由于浏览器存在同源策略,它不允许非同源的 URL 之间进行资源的交互。
- node 环境不同于浏览器环境,它不使用 XHR、Fetch 发起数据请求,也不存在浏览器的同源策略,服务器之间使用传统的 http 进行数据交互是没有跨域限制。
利用这个特性,我们其实是已经间接的解决请求跨域问题的(后面的请求跨域 (opens new window)章节中还有更多的解决方案)。
如下图所示(后面的演示案例图解):
在本地客户端中是无法获取猫眼的接口数据的
# 2、注意事项
要想实现上述的构想,要使用 node 的 https / http 的 API 方法。
以下面的案例为例:请求的数据是 https 协议的,所以使用 https 模块
- https.get()
- https.request()
# 3、具体实现
# ① 前端
- get_post.html
<body>
<h4>1、请求方式</h4>
<button class="btn-get">发起get请求</button>
<button class="btn-post">发起post请求</button>
<h4>2、服务器地址</h4>
<p>express-http服务器:<br /><br />http://localhost:5050/data</p>
<script>
document.querySelector('.btn-get').onclick = function () {
axios.get('http://localhost:5050/get/data').then((res) => {
console.log(res.data)
})
}
document.querySelector('.btn-post').onclick = function () {
axios
.post('http://localhost:5050/post/data', {
username: 'lencamo',
age: '21'
})
.then((res) => {
console.log(res.data)
})
}
</script>
</body>
# ② 后端
代理服务器 5050
- get_post.js
const http = require('http')
const url = require('url')
const server = http.createServer()
// 监听是否有🤔浏览器请求
server.on('request', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:8081')
res.setHeader('Access-Control-Allow-Methods', 'GET,POST')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
res.setHeader('Content-Type', 'application/json')
if (req.method === 'GET') {
http.get('http://localhost:6061/get/data', (response) => {
// 数据流方式返回数据(非阻塞式)
let data = ''
response.on('data', (chunk) => {
data += chunk
})
response.on('end', () => {
res.end(data)
})
})
}
if ((req.method = 'POST')) {
// 1、option配置项
let options = {
hostname: 'localhost',
port: 6061,
path: '/post/data',
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
}
// 2、获取body数据(express中就简单的多,直接 😒req.body)
let postData = ''
req.on('data', (chunk) => {
postData += chunk
})
req.on('end', () => {
// 3、发起post请求
let require = http.request(options, (response) => {
let data = ''
response.on('data', (chunk) => {
data += chunk
})
response.on('end', () => {
// 返回代理获取的数据
res.end(data)
})
})
require.write(postData)
require.end()
})
}
})
server.listen(5050, () => {
console.log('Server running at http://localhost:5050')
})
目标服务器 6061
如果不想自己搭建,可以:
- 采用猫眼的(猫眼:https://i.maoyan.com/#movie ,然后在 Fetch/XHR 随便查看一个 get 请求)
- 采用有品的(小米有品:https://m.xiaomiyoupin.com/main ,然后在 Fetch/XHR 随便查看一个 post 请求)
const express = require('express')
const app = express()
app.get('/get/data', (req, res) => {
const data = { name: 'lencamo', age: 18 }
res.send(JSON.stringify(data))
})
app.post('/post/data', (req, res) => {
const data = { code: 1, message: 'Post请求成功!' }
res.send(data)
})
app.listen(6061, () => {
console.log('HTTP服务器启动:http://localhost:6061')
})
目标服务器 在线
- get 请求:
https://i.maoyan.com/api/mmdb/movie/v3/list/hot.json?ct=%E5%8C%97%E4%BA%AC&ci=1&channelId=4
- post 请求
https:m.xiaomiyoupin.com/mtop/market/search/placeHolder
body 数据:[{}, { baseParam: { ypClient: 1 } }]
# 四、文件上传
# 1、图片处理
前端通过 form-data 格式传来的图片数据,是不能直接使用的,因为该数据存储了: Content-Disposition、name、filename、Content-Type 等附属信息,需要我们将纯粹的图片数据从中提取出来
设备识别图片时,需要图片是二进制数据
form-data 的各项数据是通过自动生成的
boundary
数据分隔开的,它存储在 content-type 中
请求示例
<body>
<input id="fileSelect" type="file" />
<button class="upload">上传图片</button>
<script>
const uploadBtn = document.querySelector('.upload')
uploadBtn.onclick = function () {
const fileList = document.querySelector('#fileSelect').files
// 收集 from-data 数据
const formData = new FormData()
formData.append('title', 'icon')
formData.append('photo', fileList[0])
// 发送 xhr 请求
var xhr = new XMLHttpRequest()
xhr.open('POST', 'http://localhost:6061/upload')
xhr.send(formData)
}
</script>
</body>
const { createServer } = require('node:http')
const { createWriteStream, writeFile } = require('node:fs')
const server = createServer((req, res) => {
if (req.url === '/upload') {
req.setEncoding('binary') // 获取设备可解析的二进制图片数据
// 进度监控
let curSize = 0
const fileSize = req.headers['content-length']
// 1、提取boundary数据
const boundary = req.headers['content-type'].split('; ')[1].replace('boundary=', '')
// 2、获取form-data数据
let formData = ''
req.on('data', (chunk) => {
// 进度监控
curSize += chunk.length
console.log('文件上传进度:', ((curSize / fileSize) * 100).toFixed(2))
formData += chunk
})
req.on('end', () => {
// console.log(formData)
const imageType = 'image/png'
const imageTypePostion = formData.indexOf(imageType) + imageType.length
// 3、提取图片数据
const imageData = formData
.substring(imageTypePostion)
.replace(/^\s\s*/, '')
.replace(`--${boundary}--`, '')
// 4、生成图片
writeFile('./icon-copy.png', imageData, { encoding: 'binary' }, (err) => {
if (err) throw err
console.log('图片处理成功')
})
})
}
res.end()
})
server.listen(6061)