Node.js從零搭建

主要是學(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)行處理的棒拂。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市玫氢,隨后出現(xiàn)的幾起案子帚屉,更是在濱河造成了極大的恐慌,老刑警劉巖漾峡,帶你破解...
    沈念sama閱讀 212,542評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件攻旦,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡灰殴,警方通過(guò)查閱死者的電腦和手機(jī)敬特,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)牺陶,“玉大人伟阔,你說(shuō)我怎么就攤上這事£欤” “怎么了皱炉?”我有些...
    開封第一講書人閱讀 158,021評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)狮鸭。 經(jīng)常有香客問(wèn)我合搅,道長(zhǎng)多搀,這世上最難降的妖魔是什么耍属? 我笑而不...
    開封第一講書人閱讀 56,682評(píng)論 1 284
  • 正文 為了忘掉前任盅称,我火速辦了婚禮极舔,結(jié)果婚禮上锚国,老公的妹妹穿的比我還像新娘。我一直安慰自己脖含,他們只是感情好比规,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,792評(píng)論 6 386
  • 文/花漫 我一把揭開白布守屉。 她就那樣靜靜地躺著锁蠕,像睡著了一般夷野。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上荣倾,一...
    開封第一講書人閱讀 49,985評(píng)論 1 291
  • 那天悯搔,我揣著相機(jī)與錄音,去河邊找鬼舌仍。 笑死妒貌,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的抡笼。 我是一名探鬼主播苏揣,決...
    沈念sama閱讀 39,107評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼推姻!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起框沟,我...
    開封第一講書人閱讀 37,845評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤藏古,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后忍燥,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拧晕,經(jīng)...
    沈念sama閱讀 44,299評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,612評(píng)論 2 327
  • 正文 我和宋清朗相戀三年梅垄,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了厂捞。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,747評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡队丝,死狀恐怖靡馁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情机久,我是刑警寧澤臭墨,帶...
    沈念sama閱讀 34,441評(píng)論 4 333
  • 正文 年R本政府宣布,位于F島的核電站膘盖,受9級(jí)特大地震影響胧弛,放射性物質(zhì)發(fā)生泄漏尤误。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,072評(píng)論 3 317
  • 文/蒙蒙 一结缚、第九天 我趴在偏房一處隱蔽的房頂上張望损晤。 院中可真熱鬧,春花似錦红竭、人聲如沸尤勋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,828評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)斥黑。三九已至,卻和暖如春眉厨,著一層夾襖步出監(jiān)牢的瞬間锌奴,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,069評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工憾股, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留鹿蜀,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,545評(píng)論 2 362
  • 正文 我出身青樓服球,卻偏偏與公主長(zhǎng)得像茴恰,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子斩熊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,658評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容