更多個(gè)人博客:(https://github.com/zenglinan/blog)
如果對你有幫助,歡迎star。
用 Node.js 可以快速搭建一個(gè) http 服務(wù)器, 本文將會手把手從頭搭建, 最后還會實(shí)現(xiàn)一個(gè)簡易的 express 服務(wù)器, 開始吧~
1. 搭建基本雛形
首先, 搭建 http 服務(wù)器, 需要先引用 Node.js 的核心模塊: http, 實(shí)際上這是一個(gè)對象, 利用 http 上的 createServer 方法來創(chuàng)建一個(gè) http 服務(wù)器, createServer 方法需要接受一個(gè)事件函數(shù), 用于處理請求和響應(yīng)
const http = require('http')
const server = http.createServer((req, res) => {
// req 是請求對象, 可以獲取請求的一些方法路徑數(shù)據(jù)等屬性
// res 是響應(yīng)對象, 可以進(jìn)行設(shè)置響應(yīng)數(shù)據(jù)等操作
})
到現(xiàn)在, 這個(gè) http 服務(wù)器的雛形已經(jīng)基本搭建好了!
但是想想還差點(diǎn)東西, 一個(gè) url 里包含 協(xié)議 域名 端口, 我們還沒指定端口呢.
const http = require('http')
const server = http.createServer((req, res) => {
})
server.listen(8000) // 監(jiān)聽 8000 端口
OK, 大功告成
2. 響應(yīng)數(shù)據(jù)
現(xiàn)在這個(gè) http 服務(wù)器的框架已經(jīng)搭好了. 啟動(dòng)一下, 在瀏覽器輸入localhost:8000
試一下吧
什么? 你說你試了一下, 沒有響應(yīng)?
當(dāng)然, 我們這里還沒有返回任何數(shù)據(jù)呢, 如果沒有訪問數(shù)據(jù), 瀏覽器端肯定是顯示無響應(yīng)的.
這里我們先隨便返回點(diǎn)數(shù)據(jù)給瀏覽器, 然后重啟服務(wù)器
const http = require('http')
const server = http.createServer((req, res) => {
res.end('這是我返回的數(shù)據(jù)噢!')
})
server.listen(8000)
相信你已經(jīng)看到頁面上顯示的....一堆亂碼了吧_, 是的, 因?yàn)?JavaScript 默認(rèn)字符集對中文的支持不好. 我們需要指定一下返回?cái)?shù)據(jù)的 Content-Type
const http = require('http')
const server = http.createServer((req, res) => {
res.setHeader("Content-Type","text/html;charset=utf-8")
res.end('這是我返回的數(shù)據(jù)噢!')
})
server.listen(8000)
3. 處理路由
接下來, 我們需要對路由進(jìn)行處理, 現(xiàn)在我們不管訪問什么路徑, 都統(tǒng)一返回一樣的數(shù)據(jù).
接下來我們實(shí)現(xiàn)一下, 訪問 /a , /b, /c 三個(gè)路由, 返回不同的數(shù)據(jù)
之前說過, req 對象上存放著請求的一些屬性. req.url
上記錄著請求的路徑, 獲取后就可以判斷訪問路徑來返回不同的數(shù)據(jù)了
const {url} = req
完整代碼:
const http = require('http')
const server = http.createServer((req, res) => {
const {url} = req
res.setHeader("Content-Type","text/html;charset=utf-8")
if(url === '/a') res.end(`訪問a路由`)
else if(url === '/b') res.end(`訪問b路由`)
else if(url === '/c') res.end(`訪問c路由`)
})
server.listen(8000)
4. 處理查詢參數(shù)
接下來, 我們對查詢參數(shù)做一下處理, 這時(shí)候, 是不是想到了什么, 上面我們的 url 沒有考慮到查詢參數(shù)的情況, 路由里應(yīng)該濾除掉查詢參數(shù), 我們來一并處理
我們知道, 查詢參數(shù)的形式是 a=x&b=x
, 這里為了方便使用, 我們引用另一個(gè)模塊 querystring
, 他可以把查詢參數(shù)字符串切割成鍵值對形式
const http = require('http')
const querystring = require('querystring')
const server = http.createServer((req, res) => {
const {url} = req
const path = url.split('?')[0]
const query = querystring.parse(url.split('?')[1]) // 保存著查詢參數(shù)的對象
res.setHeader("Content-Type","text/html;charset=utf-8")
if(path === '/a') res.end(`訪問a路由, 查詢參數(shù)對象為${JSON.stringify(query)}`) // 返回序列化的查詢參數(shù)
else if(path === '/b') res.end(`訪問b路由`)
else if(path === '/c') res.end(`訪問c路由`)
})
server.listen(8000)
5.處理 POST 請求
OK, 接下來要做啥呢? 想了想, 我們目前好像只處理了 GET 請求, 那我們來處理一下 POST 請求吧.
一樣的, 請求的 method 可以通過req.method
獲取
這里要注意: req 對象實(shí)現(xiàn)了 ReadableStream 接口, 我們可以用信息流的方式讀取傳來的數(shù)據(jù) (關(guān)于流的概念可以看后面我的文章)
let postData = ''
req.on('data', chunk => { // 接收數(shù)據(jù)流
postData += chunk.toString() // 拼接信息流, 注意chunk是二進(jìn)制格式, 需要轉(zhuǎn)為二進(jìn)制
})
req.on('end', () => {
// 接收數(shù)據(jù)流完畢的回調(diào)函數(shù)
})
完整代碼 :
const http = require('http')
const querystring = require('querystring')
const server = http.createServer((req, res) => {
const {url, method} = req
const path = url.split('?')[0]
const query = querystring.parse(url.split('?')[1])
res.setHeader("Content-Type","text/html;charset=utf-8")
if(path === '/a' && method === 'GET') res.end(`訪問a路由, 查詢參數(shù)對象為${JSON.stringify(query)}`)
else if(path === '/b' && method === 'GET') res.end(`訪問b路由`)
else if(path === '/c' && method === 'GET') res.end(`訪問c路由`)
else if(path === 'p' && method === 'POST'){
let postData = ''
req.on('data', chunk => { // 接收數(shù)據(jù)流
postData += chunk.toString() // 拼接信息流, 注意chunk是二進(jìn)制格式, 需要轉(zhuǎn)為二進(jìn)制
})
req.on('end', () => {
res.end(`我接受到了數(shù)據(jù)了:${postData}`)
})
}
})
server.listen(8000)
OK, 來回顧一下我們做了什么:
- 我們創(chuàng)建了一個(gè)基本的 http 服務(wù)器
- 對路由做了處理
- 獲取了查詢參數(shù)
- 處理了 POST 請求
6. 優(yōu)化路由處理
我們現(xiàn)在來回顧一下自己的代碼, 可以看到, 在路由處理的部分有一堆的 if else, 假如每多一個(gè)路由就多一個(gè) if , 那就太冗余了.
這里我們用一個(gè)數(shù)組來存放一個(gè)個(gè)路由對象, 路由對象里包含了路徑, 方法, 回調(diào)等必要信息
const http = require('http')
const querystring = require('querystring')
const server = http.createServer((req, res) => {
const {url, method: realMethod} = req
const realPath= url.split('?')[0]
const query = querystring.parse(url.split('?')[1])
let router = [] // 存放路由信息
res.setHeader("Content-Type","text/html;charset=utf-8")
router.push({
path: '/a',
method: 'GET',
handler(req, res){
res.end(`訪問a路由, 查詢參數(shù)對象為${JSON.stringify(query)}`)
}
})
router.push({
path: '/b',
method: 'GET',
handler(req, res){
res.end(`訪問b路由`)
}
})
router.push({
path: '/c',
method: 'GET',
handler(req, res){
res.end(`訪問c路由`)
}
})
router.push({
path: '/p',
method: 'POST',
handler(req, res){
let postData = ''
req.on('data', chunk => {
postData += chunk.toString()
})
req.on('end', () => {
res.end(`我接受到了數(shù)據(jù)了:${postData}`)
})
}
})
// 統(tǒng)一處理路由
router.forEach(route => {
let {path, method, handler} = route
console.log(realPath, realMethod)
if(realPath === path && realMethod === method){
return handler()
}
})
})
server.listen(8000)
7. 改寫為 express 形式
是不是感覺稍微好看一點(diǎn)了, 加一個(gè)路由就 push 一個(gè)路由對象.
我們離最終目標(biāo)很接近了, 接下來, 讓我們模仿 express , 寫一個(gè) express 形式的 http 服務(wù)器(▽)
先來看看 express 是怎么寫的
const express = require("express");
const app = express();
app.get("/a",
(req, res) => {
res.end("a路由");
}
);
app.get("/b",
(req, res) => {
res.end('b路由');
});
app.listen(3000, () => {
console.log("Example app listen at 3000");
});
可以看到, 導(dǎo)出的 express 是一個(gè)函數(shù), 函數(shù)內(nèi)部會 new 一個(gè)實(shí)例對象出來, 大概的架子便是這樣.
const http = require('http')
class Express{
}
module.exports = function(){
return new Express()
}
接下來讓我們實(shí)現(xiàn)完整代碼:
const http = require('http')
class Express{
constructor(){
this.router = [] // 存放路由對象
}
get(path, handler){
this.router.push({
path,
method: 'GET',
handler
})
}
post(path, handler){
this.router.push({
path,
method: 'POST',
handler
})
}
listen(port, listenCallback){
const server = http.createServer((req,res) => {
const {url, method:realMethod} = req
const realPath = url.split('?')[0]
this.router.forEach((route) => { // 遍歷路由對象
const {path, method, handler} = route
if(realPath === path && method === realMethod){
handler(req, res)
}
})
})
server.listen(port)
listenCallback()
}
}
module.exports = function(){
return new Express()
}
到這里, 我們已經(jīng)簡單將我們的 http 服務(wù)器改寫成 express 形式了, 不過考慮的細(xì)節(jié)還遠(yuǎn)遠(yuǎn)不夠 express 那么完善.
撒花ヾ(?°?°?)??