主要是學(xué)習(xí)了Node.js從零開發(fā)Web Server博客,而將學(xué)習(xí)內(nèi)容做個(gè)總結(jié)赋焕。
1.nodejs介紹
nodejs的安裝就不說(shuō)了榜揖,最主要的是安裝nodemon和cross-env。
1)nodemon
nodemon主要是用來(lái)當(dāng)服務(wù)器啟動(dòng)之后鸽素,監(jiān)聽(tīng)文件的變化褒繁,當(dāng)有文件發(fā)生變化的時(shí)候,就會(huì)自動(dòng)重啟服務(wù)馍忽。
2)cross-env
cross-env主要是在package.json里面設(shè)置環(huán)境變量棒坏。可以讓程序內(nèi)通過(guò)process.env來(lái)進(jìn)行獲取變量遭笋。
例如:在package.json的script中寫上了
cross-env NODE_ENV=dev nodemon ./bin/www.js
意思就是啟動(dòng)www.js這個(gè)主文件坝冕,然后可以在process.env.NODE_ENV獲取到dev這個(gè)值。
2.主體框架搭建
1)創(chuàng)建主入口文件
首先在項(xiàng)目文件夾內(nèi)創(chuàng)建bin文件夾瓦呼,然后在里面創(chuàng)建www.js喂窟,該文件主要是用來(lái)啟動(dòng)服務(wù)的,是項(xiàng)目的主要入口吵血。
const http = require('http')
const PORT = 8000
const serverHandle = require('../app')
const server = http.createServer(serverHandle)
server.listen(PORT, () => {
console.log('server listen on localhost:8000')
})
利用nodejs自帶的http創(chuàng)建一個(gè)服務(wù)器實(shí)例谎替,并傳入一個(gè)函數(shù),當(dāng)用戶訪問(wèn)的時(shí)候會(huì)走這一個(gè)函數(shù)蹋辅。由于這只是主入口钱贯,應(yīng)做到代碼分離,這樣改起來(lái)比較清晰一點(diǎn)侦另。所以將函數(shù)放在了另一個(gè)文件當(dāng)中秩命,最后server.listen進(jìn)行監(jiān)聽(tīng)8000端口就可以了。
入口函數(shù)可以什么都不寫褒傅,然后用node ./bin/www.js也可以進(jìn)行啟動(dòng)弃锐。
2)創(chuàng)建入口函數(shù)
首先我們?cè)诟夸浵聞?chuàng)建app.js作為處理用戶訪問(wèn)時(shí)候的函數(shù)。
該函數(shù)主要接受兩個(gè)參數(shù)req殿托,res霹菊,即請(qǐng)求和響應(yīng)。
主體內(nèi)容為:
const serverHandle = async (req, res) => {
}
module.exports = serverHandle
這樣其實(shí)就已經(jīng)可以進(jìn)行輸出了支竹,而程序主要做的就是往里面填寫東西旋廷,對(duì)用戶訪問(wèn)進(jìn)行的請(qǐng)求進(jìn)行處理,并添加處理結(jié)果到響應(yīng)中礼搁,返回給用戶饶碘。
3)解析url地址
通過(guò)req.url我們可以獲取到用戶訪問(wèn)的路徑,然后使用split(‘?’)來(lái)分別對(duì)路徑的地址和參數(shù)作出處理馒吴。
將地址存入req.path中扎运,方便路由的時(shí)候進(jìn)行處理瑟曲。
const url = req.url
req.path = url.split('?')[0]
對(duì)參數(shù)部分的處理,可以通過(guò)引入const querystring = require('querystring')來(lái)進(jìn)行處理豪治。只需要一句話就可以將a=2&b=3轉(zhuǎn)換成對(duì)象{a:2,b:3}的形式
然后存入req.query當(dāng)中洞拨。
req.query = querystring.parse(url.split('?')[1])
4)對(duì)post的data進(jìn)行處理
由于獲取data數(shù)據(jù)的時(shí)候,是需要一點(diǎn)點(diǎn)獲取的鬼吵,所以要使用req.on('data')函數(shù)來(lái)進(jìn)行獲取數(shù)據(jù)扣甲,因?yàn)樵摂?shù)據(jù)是二進(jìn)制的形式篮赢,所以需要轉(zhuǎn)換成字符串齿椅,然后使用req.on('end')來(lái)進(jìn)行監(jiān)聽(tīng)是否完成數(shù)據(jù)的接受。
具體代碼如下:
const getPostData = req => {
let promise = new Promise((resolve, reject) => {
if (req.method == 'GET') {
resolve({})
return
}
if (req.headers['content-type'] !== 'application/json') {
resolve({})
return
}
let postData = ''
req.on('data', chunk => {
postData += chunk.toString()
})
req.on('end', () => {
if (!postData) {
resolve({})
return
}
resolve(JSON.parse(postData))
})
})
return promise
}
這里主要使用了promise的方式來(lái)檢測(cè)是否完成启泣,方便后面使用async涣脚、await的方式來(lái)進(jìn)行同步的操作。因?yàn)檫@部分?jǐn)?shù)據(jù)沒(méi)有獲取完之前是不能對(duì)數(shù)據(jù)進(jìn)行獲取寥茫,并做處理的遣蚀。
前面只是對(duì)method方式為get就返回,content-type不為application/json的就返回纱耻,實(shí)際上還有很多種情況芭梯。form表單提交等也可以作處理。
所以這一部分放在serverHandle 的開頭就可以的弄喘。然后將獲取到的數(shù)據(jù)放入req.body當(dāng)中玖喘,方便后續(xù)操作。
3.路由操作
1)主體框架
路由的原理其實(shí)就是對(duì)req.path進(jìn)行判斷蘑志,是否和路徑對(duì)應(yīng)累奈,對(duì)應(yīng)則走這一步函數(shù),沒(méi)有對(duì)應(yīng)則不作處理急但。
首先創(chuàng)建一個(gè)route的文件夾澎媒,并且根據(jù)模塊的不同,創(chuàng)建route文件波桩。
然后在app.js中引入該route文件戒努。
const handleBlogRouter = require('./src/router/blog')
在serverHandle中調(diào)用該方法,該函數(shù)會(huì)返回一個(gè)promise對(duì)象镐躲,可以根據(jù)該對(duì)象的狀態(tài)來(lái)判斷是否執(zhí)行储玫,如果執(zhí)行了就輸出返回給用戶。
2)內(nèi)部操作
在路由的內(nèi)部需要判斷的有兩點(diǎn)匀油。(1)method缘缚,客戶端傳入的方法是get,post敌蚜,delete還是put桥滨。(2)判斷地址。
(1)method
可以通過(guò)req.method來(lái)進(jìn)行獲取。
(2)判斷地址
這里主要用到的是RESTful API的形式來(lái)進(jìn)行的齐媒。
比如get中的/api/blog 和/api/blog/:id 同屬于/api/blog/ 所以這里就需要進(jìn)行判斷蒲每。
const num = req.path.split('/').pop()
let numParam = true
if (isNaN(Number(num))) {
numParam = false
}
// 獲取博客列表
if (method === 'GET' && req.path === '/api/blog' && !numParam) {
...
}
//// 獲取博客詳情
if (method === 'GET' && req.path.indexOf('/api/blog') !== -1 && numParam) {
...
}
numParam是用來(lái)判斷:id是否為數(shù)字的。只有是數(shù)字的情況下才能進(jìn)行查找詳情內(nèi)容喻括。這只是簡(jiǎn)略的判斷邀杏,最好還是使用正則來(lái)進(jìn)行判斷。
另外的像post唬血,delete望蜡,put就不多說(shuō)了,其實(shí)原理都和get方式的是一樣的拷恨。
4.數(shù)據(jù)庫(kù)操作
在介紹完路由之后脖律,客戶端就需要在對(duì)應(yīng)的api地址中得到返回,那么路由里面需要執(zhí)行的就是邏輯代碼和進(jìn)行數(shù)據(jù)庫(kù)處理了腕侄。并且返回?cái)?shù)據(jù)了小泉。
1)配置文件
首先在根目錄下創(chuàng)建conf文件夾,用來(lái)存放數(shù)據(jù)庫(kù)密碼等冕杠。
這個(gè)時(shí)候就可以用到cross-env了微姊,還記得我們?cè)趐ackage.json中配置了這么一段話么?
"scripts": {
"dev": "cross-env NODE_ENV=dev nodemon ./bin/www.js"
}
這個(gè)時(shí)候就可以用到這個(gè)環(huán)境變量了分预。通過(guò)生產(chǎn)環(huán)境的不同而導(dǎo)出不同的數(shù)據(jù)庫(kù)信息兢交,就可以做到在開發(fā)和上線的時(shí)候,不用頻繁變更數(shù)據(jù)庫(kù)的信息了噪舀。
//獲取環(huán)境變量
let env = process.env.NODE_ENV
let MYSQL_CONF = {}
if (env === 'dev') {
MYSQL_CONF = {
host: 'localhost',
user: 'root',
password: '',
port: '3306',
database: 'myblog'
}
}
if (env === 'production') {
...
}
2)配置mysql魁淳,編寫執(zhí)行函數(shù)
我們需要在根目錄下創(chuàng)建一個(gè)db文件夾,用來(lái)存放數(shù)據(jù)庫(kù)的相關(guān)操作与倡。然后通過(guò)npm install mysql來(lái)對(duì)mysql進(jìn)行安裝界逛。
導(dǎo)入mysql,導(dǎo)入配置文件纺座,然后創(chuàng)建mysql連接實(shí)例con息拜。使用con.connect()進(jìn)行連接數(shù)據(jù)庫(kù)。
通過(guò)con.query方法來(lái)執(zhí)行sql語(yǔ)句净响,這里創(chuàng)建了一個(gè)通用的方法導(dǎo)出少欺,方便后續(xù)直接使用該方法來(lái)執(zhí)行sql語(yǔ)句。
con.query第一個(gè)為sql語(yǔ)句馋贤,第二個(gè)為回調(diào)函數(shù)赞别。
const mysql = require('mysql')
const { MYSQL_CONF } = require('../conf/db')
const con = mysql.createConnection(MYSQL_CONF)
// 開始連接
con.connect()
// 執(zhí)行sql語(yǔ)句
function exec(sql) {
return new Promise((resolve, reject) => {
con.query(sql, (error, result) => {
if (error) {
console.log(error)
reject(error)
return
}
resolve(result)
})
})
}
當(dāng)進(jìn)行完這些步驟的時(shí)候,我們就可以正式開始業(yè)務(wù)代碼的編寫了配乓,只需要在執(zhí)行完之后返回mysql的exec方法即可仿滔。
5.session和redis
session和redis主要是用于登錄模塊惠毁,由于有些api需要登錄了之后才能查看,比如單獨(dú)用戶的操作崎页,發(fā)文章等等鞠绰。
session的原理就是在服務(wù)器開啟的時(shí)候使用一個(gè)全局變量,然后將登錄的信息存入全局變量當(dāng)中飒焦,相當(dāng)于放在了進(jìn)程的內(nèi)存當(dāng)中蜈膨,每次用戶訪問(wèn)的時(shí)候就根據(jù)cookie來(lái)看在session中是否有信息,有的話就處于登錄狀態(tài)牺荠,否則就需要登錄翁巍。
弊端:
(1)服務(wù)重啟了之后,變量就會(huì)消失志电。
(2)進(jìn)程內(nèi)存有限曙咽,訪問(wèn)量過(guò)大,內(nèi)存會(huì)暴增挑辆。
(3)上線 之后為多線程,多線程之間內(nèi)存無(wú)法共享孝情。
由于存在著以上的弊端鱼蝉,所以則需要使用redis來(lái)進(jìn)行存儲(chǔ)cookie。redis相當(dāng)于一個(gè)獨(dú)立的個(gè)體箫荡,多線程之間也不會(huì)出先數(shù)據(jù)無(wú)法共存的情況魁亦。而且服務(wù)重啟的時(shí)候,redis還依然在運(yùn)行羔挡。
1)解析cookie
判斷用戶是否登錄除了token之外就是使用cookie了洁奈。而且cookie可以是服務(wù)端在res中添加,并且返回到客戶端绞灼±酰客戶端就會(huì)帶上cookie的信息了。
我們可以通過(guò)req.headers.cookie來(lái)獲取到客戶端返回的cookie低矮。然后將其轉(zhuǎn)換成對(duì)象印叁。
req.cookie = {}
const cookieStr = req.headers.cookie || ''
cookieStr.split(';').forEach(item => {
if (!item) {
return
}
const arr = item.split('=')
const key = arr[0].trim()
const value = arr[1].trim()
req.cookie[key] = value
})
而在登錄完之后,則需要生成一個(gè)userId军掂,然后放在返回的headers當(dāng)中轮蜕,這時(shí)候客戶端就會(huì)有該cookie了
res.setHeader(
'Set-Cookie',
`userId=${userId} ; path=/; httpOnly; expires=${getOneDay()}`
)
幾個(gè)注意的點(diǎn):
(1)cookie做限制
需要在服務(wù)端res.setHeader(’Set-Cookie‘, 'xxx=xxx')上在最后加一句httpOnly,防止被篡改蝗锥。
(2)加上時(shí)間
在最后加expires= xxx表示有效期截止時(shí)間跃洛。
2)配置redis
和配置mysql方法差不多,需要在conf文件中配置redis的基本數(shù)據(jù)终议。然后編寫一個(gè)get汇竭,set闲延,這里主要用到的只有這兩個(gè)方法。再將這兩個(gè)方法導(dǎo)出就可以了韩玩。
conf文件中redis配置
REDIS_CONF = {
port: '6379',
host: '127.0.0.1'
}
在db文件夾中創(chuàng)建redis文件垒玲。創(chuàng)建一個(gè)redis實(shí)例,監(jiān)聽(tīng)錯(cuò)誤的方法找颓,然后導(dǎo)出get和set方法合愈。
注:存儲(chǔ)的是字符串而不是對(duì)象
const redisClient = redis.createClient(REDIS_CONF.port, REDIS_CONF.host)
redisClient.on('error', function(err) {
console.log(err)
})
function set(key, value) {
if (typeof value === 'object') {
value = JSON.stringify(value)
}
redisClient.set(key, value)
}
function get(key) {
return new Promise((resolve, reject) => {
redisClient.get(key, function(err, value) {
if (err) {
reject(err)
}
if (!value) {
resolve(null)
}
try {
resolve(JSON.parse(value))
} catch (e) {
resolve(value)
}
})
})
}
3)存儲(chǔ)redis
當(dāng)做完配置的工作之后,使用redis大致的步驟就是:
(1)解析cookie击狮。
(2)看cookie中是否存在userId
(3)不存在則創(chuàng)建佛析,存在則去redis中查找是否存在該userId,以此來(lái)判斷用戶是否登錄
(4)最后將結(jié)果賦值到req.session中彪蓬,方便在個(gè)人操作時(shí)判斷req.session的值來(lái)看是否能進(jìn)行操作
// 處理redis
let needSetCookie = false
let userId = req.cookie.userId
if (!userId) {
userId = Date.now() + '_' + Math.random()
needSetCookie = true
}
req.sessionId = userId
let result = await get(req.sessionId)
if (result == null) {
set(req.sessionId, {})
// 設(shè)置session
req.session = {}
} else {
req.session = result
}
然后在登錄的時(shí)候?qū)eq.session中的userId保存到redis中即可寸莫。
const result = await login(username, password)
console.log(result)
if (result[0]) {
// 設(shè)置cookie
req.session.username = result[0].username
set(req.sessionId, req.session)
}
6.總結(jié)
在沒(méi)有使用express和koa的情況下,主要是為了分析express和koa的底層實(shí)現(xiàn)原理档冬,主要是為了更好的理解框架本身膘茎,在實(shí)踐過(guò)程中還是需要用到express和koa的,畢竟為了項(xiàng)目能快速上線酷誓,并不是所有都需要從零開始搭建的披坏。
至此大致的主體框架已經(jīng)基本完成了,還剩下的主要就是系統(tǒng)日志的寫入和對(duì)錯(cuò)誤的處理盐数。這一部分通過(guò)express和koa的中間件都能很好的進(jìn)行處理的棒拂。