阅读更多系列文章请访问我的GitHub博客,示例代码请访问这里。
创建服务器
在实现了路由之后,就可以以此为基础实现服务器了。
实现服务器分为以下几个步骤:
示例代码:/lesson28/lib/http.js
- 引入所需Node.js模块、服务器配置、路由模块
- 封装统一处理请求数据的方法
- 接收到的请求分为POST请求、GET请求,区分并进行处理
- POST请求分为数据请求、文件上传请求,区分并进行处理
- GET请求分为数据请求、读取文件请求,区分并进行处理
接下来,按步骤实现每部分代码。
1. 引入所需Node.js模块、服务器配置、路由模块
// 引入创建服务器所需的模块
const http = require('http')
const url = require('url')
const querystring = require('querystring')
const zlib = require('zlib')
const fs = require('fs')
const { Form } = require('multiparty')
// 引入服务器配置
const {
HTTP_PORT,
HTTP_ROOT,
HTTP_UPLOAD
} = require('../config')
// 引入路由模块的查找路由方法
const { findRouter } = require('./router')
const server = http.createServer((req, res) => {
// 服务器代码
})
// 监听配置的端口
server.listen(HTTP_PORT)
// 打印创建服务器成功信息
console.log(`Server started at ${HTTP_PORT}`)
2. 封装统一处理请求数据的方法
要处理所有请求接口,需要的参数为method(请求方法)、pathname(请求接口路径)、query(query数据)、post(post数据)、files(文件数据)。
首先,根据method(请求方法)、pathname(请求接口路径),获取在路由配置时,已经配置好的相应接口的回调函数。
其次,若回调函数存在,则直接将参数传入回调函数处理。
最后,若回调函数不存在,则默认为请求一个静态文件,即可将文件读取之后发送给前端。
// 引入创建服务器所需的模块
...
// 引入服务器配置
...
// 引入路由模块的查找路由方法
...
const server = http.createServer((req, res) => {
// 通过路由处理请求数据的公共方法
async function processData(method, pathname, query, post, files) {
const callback = findRouter(method, pathname) // 获取处理请求的回调函数
// 若回调函数存在,则表示路由有配置相应的数据处理,即该请求不是获取静态文件。
if (callback) {
try {
// 根据路由处理接口数据
await callback(res, query, post, files)
} catch (error) {
// 出现错误的处理
res.writeHead(500)
res.write('Internal Server Error')
res.end()
}
} else {
// 若回调函数不存在,则表示该请求为请求一个静态文件,如html、css、js等
...
}
}
})
// 监听配置的端口
server.listen(HTTP_PORT)
// 打印创建服务器成功信息
console.log(`Server started at ${HTTP_PORT}`)
3. 接收到的请求分为POST请求、GET请求,区分并进行处理
根据请求的method,将请求分为POST请求、GET请求。
若为POST请求,则需要进一步判断是普通数据请求,还是文件请求,并分别进行处理。
而GET请求,只需要将数据传入processData方法进行处理,在processData方法中,区分GET请求获取数据,还是获取静态文件。
// 引入创建服务器所需的模块
...
// 引入服务器配置
...
// 引入路由模块的查找路由方法
...
const server = http.createServer((req, res) => {
// 解析请求数据
// 获取请求路径及query数据
const method = req.method
const {
pathname,
query
} = url.parse(req.url, true)
// 处理POST请求
if (method === 'POST') {
// POST请求分为数据请求、文件上传请求,区分并进行处理
...
} else { // 处理GET请求
// 通过路由处理数据,因为此时是GET请求,只有query数据
processData(method, url, query, {}, {})
}
// 通过路由处理请求数据的公共方法
async function processData(method, pathname, query, post, files) {
...
}
})
// 监听配置的端口
server.listen(HTTP_PORT)
// 打印创建服务器成功信息
console.log(`Server started at ${HTTP_PORT}`)
4. POST请求分为数据请求、文件上传请求,区分并进行处理
判断请求头的content-type为application/x-www-form-urlencoded时,表示该请求只是单纯传输数据,可以直接当做字符串处理。
若请求头的content-type不对,则表示该请求是上传文件,可以用multiparty进行处理。
// 引入创建服务器所需的模块
...
// 引入服务器配置
...
// 引入路由模块的查找路由方法
...
const server = http.createServer((req, res) => {
// 解析请求数据
// 获取请求路径及query数据
const method = req.method
const {
pathname,
query
} = url.parse(req.url, true)
// 处理POST请求
if (method === 'POST') {
// 根据请求头的content-type属性值,区分是普通POST请求,还是文件请求。
// content-type为application/x-www-form-urlencoded时,表示是普通POST请求
// 普通POST请求直接进行处理,文件请求使用multiparty处理
if (req.headers['content-type'].startsWith('application/x-www-form-urlencoded')) {
// 普通POST请求
let arr = [] // 存储Buffer数据
// 接收数据
req.on('data', (buffer) => {
arr.push(buffer)
})
// 数据接收完成
req.on('end', () => {
const data = Buffer.concat(arr) // 合并接收到的数据
const post = querystring.parse(data.toString()) // 将接收到的数据转换为JSON
// 通过路由处理数据,因为此时是普通POST请求,不存在文件数据
processData(method, pathname, query, post, {})
})
} else {
// 文件POST请求
const form = new Form({
uploadDir: HTTP_UPLOAD // 指定文件存储目录
})
// 处理请求数据
form.parse(req)
let post = {} // 存储数据参数
let files = {} // 存储文件数据
// 通过field事件处理普通数据
form.on('field', (name, value) => {
post[name] = value
})
// 通过file时间处理文件数据
form.on('file', (name, file) => {
files[name] = file
})
// 处理错误
form.on('error', (error) => {
console.error(error)
})
// 数据传输完成时,触发close事件
form.on('close', () => {
// 通过路由处理数据,因为此时是POST文件请求,query、post、files数据都存在
processData(method, pathname, query, post, files)
})
}
} else { // 处理GET请求
// 通过路由处理数据,因为此时是GET请求,只有query数据
processData(method, url, query, {}, {})
}
// 通过路由处理请求数据的公共方法
async function processData(method, pathname, query, post, files) {
...
}
})
// 监听配置的端口
server.listen(HTTP_PORT)
// 打印创建服务器成功信息
console.log(`Server started at ${HTTP_PORT}`)
5. GET请求分为数据请求、读取文件请求,区分并进行处理
GET请求可以直接用processData方法统一处理,若路由中未配置处理数据的方法,则表示该请求为获取静态文件,需要进行单独处理,否则只需要调用路由配置的回调函数处理即可。
// 引入创建服务器所需的模块
...
// 引入服务器配置
...
// 引入路由模块的查找路由方法
...
const server = http.createServer((req, res) => {
// 解析请求数据
// 获取请求路径及query数据
const method = req.method
const {
pathname,
query
} = url.parse(req.url, true)
// 处理POST请求
if (method === 'POST') {
...
} else { // 处理GET请求
// 通过路由处理数据,因为此时是GET请求,只有query数据
processData(method, url, query, {}, {})
}
// 通过路由处理请求数据的公共方法
async function processData(method, pathname, query, post, files) {
const callback = findRouter(method, pathname) // 获取处理请求的回调函数
// 若回调函数存在,则表示路由有配置相应的数据处理,即该请求不是获取静态文件。
if (callback) {
try {
// 根据路由处理接口数据
await callback(res, query, post, files)
} catch (error) {
// 出现错误的处理
res.writeHead(500)
res.write('Internal Server Error')
res.end()
}
} else {
// 若回调函数不存在,则表示该请求为请求一个静态文件,如html、css、js等
const filePath = HTTP_ROOT + pathname
// 检查文件是否存在
fs.stat(filePath, (error, stat) => {
if (error) {
// 出现错误表示文件不存在
res.writeHead(404)
res.write('Not Found')
res.end()
} else {
// 文件存在则进行读取
// 创建一个可读流。
const readStream = fs.createReadStream(filePath)
// 创建一个Gzip对象,用于将文件压缩成
const gz = zlib.createGzip()
// 向浏览器发送经过gzip压缩的文件,设置响应头,否则浏览器无法识别,会自动进行下载。
res.setHeader('content-encoding', 'gzip')
// 将读取的内容,通过gzip压缩之后,在通过管道推送到res中,由于res继承自Stream流,因此也可以接收管道的推送。
readStream.pipe(gz).pipe(res)
readStream.on('error', (error) => {
console.error(error)
})
}
})
}
}
})
// 监听配置的端口
server.listen(HTTP_PORT)
// 打印创建服务器成功信息
console.log(`Server started at ${HTTP_PORT}`)
测试服务器
在server.js中引入封装的http模块:
const http = require('./lib/http')
再使用node server.js启动服务器,就可以在浏览器中访问http://localhost:8080/index.html,看到html页面。