Node.js從零搭建

主要是學習了Node.js從零開發(fā)Web Server博客,而將學習內(nèi)容做個總結(jié)。

1.nodejs介紹

nodejs的安裝就不說了,最主要的是安裝nodemon和cross-env。

1)nodemon

nodemon主要是用來當服務器啟動之后,監(jiān)聽文件的變化,當有文件發(fā)生變化的時候,就會自動重啟服務。

2)cross-env

cross-env主要是在package.json里面設置環(huán)境變量??梢宰尦绦騼?nèi)通過process.env來進行獲取變量。
例如:在package.json的script中寫上了
cross-env NODE_ENV=dev nodemon ./bin/www.js
意思就是啟動www.js這個主文件,然后可以在process.env.NODE_ENV獲取到dev這個值。

2.主體框架搭建

1)創(chuàng)建主入口文件

首先在項目文件夾內(nèi)創(chuàng)建bin文件夾,然后在里面創(chuàng)建www.js,該文件主要是用來啟動服務的,是項目的主要入口。

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)建一個服務器實例,并傳入一個函數(shù),當用戶訪問的時候會走這一個函數(shù)。由于這只是主入口,應做到代碼分離,這樣改起來比較清晰一點。所以將函數(shù)放在了另一個文件當中,最后server.listen進行監(jiān)聽8000端口就可以了。
入口函數(shù)可以什么都不寫,然后用node ./bin/www.js也可以進行啟動。

2)創(chuàng)建入口函數(shù)

首先我們在根目錄下創(chuàng)建app.js作為處理用戶訪問時候的函數(shù)。
該函數(shù)主要接受兩個參數(shù)req,res,即請求和響應。
主體內(nèi)容為:

const serverHandle = async (req, res) => {
}
module.exports = serverHandle

這樣其實就已經(jīng)可以進行輸出了,而程序主要做的就是往里面填寫東西,對用戶訪問進行的請求進行處理,并添加處理結(jié)果到響應中,返回給用戶。

3)解析url地址

通過req.url我們可以獲取到用戶訪問的路徑,然后使用split(‘?’)來分別對路徑的地址和參數(shù)作出處理。
將地址存入req.path中,方便路由的時候進行處理。

  const url = req.url
  req.path = url.split('?')[0]

對參數(shù)部分的處理,可以通過引入const querystring = require('querystring')來進行處理。只需要一句話就可以將a=2&b=3轉(zhuǎn)換成對象{a:2,b:3}的形式
然后存入req.query當中。

req.query = querystring.parse(url.split('?')[1])

4)對post的data進行處理

由于獲取data數(shù)據(jù)的時候,是需要一點點獲取的,所以要使用req.on('data')函數(shù)來進行獲取數(shù)據(jù),因為該數(shù)據(jù)是二進制的形式,所以需要轉(zhuǎn)換成字符串,然后使用req.on('end')來進行監(jiān)聽是否完成數(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的方式來檢測是否完成,方便后面使用async、await的方式來進行同步的操作。因為這部分數(shù)據(jù)沒有獲取完之前是不能對數(shù)據(jù)進行獲取,并做處理的。
前面只是對method方式為get就返回,content-type不為application/json的就返回,實際上還有很多種情況。form表單提交等也可以作處理。

所以這一部分放在serverHandle 的開頭就可以的。然后將獲取到的數(shù)據(jù)放入req.body當中,方便后續(xù)操作。

3.路由操作

1)主體框架

路由的原理其實就是對req.path進行判斷,是否和路徑對應,對應則走這一步函數(shù),沒有對應則不作處理。
首先創(chuàng)建一個route的文件夾,并且根據(jù)模塊的不同,創(chuàng)建route文件。
然后在app.js中引入該route文件。

const handleBlogRouter = require('./src/router/blog')

在serverHandle中調(diào)用該方法,該函數(shù)會返回一個promise對象,可以根據(jù)該對象的狀態(tài)來判斷是否執(zhí)行,如果執(zhí)行了就輸出返回給用戶。

2)內(nèi)部操作

在路由的內(nèi)部需要判斷的有兩點。(1)method,客戶端傳入的方法是get,post,delete還是put。(2)判斷地址。

(1)method

可以通過req.method來進行獲取。

(2)判斷地址

這里主要用到的是RESTful API的形式來進行的。
比如get中的/api/blog 和/api/blog/:id 同屬于/api/blog/ 所以這里就需要進行判斷。

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是用來判斷:id是否為數(shù)字的。只有是數(shù)字的情況下才能進行查找詳情內(nèi)容。這只是簡略的判斷,最好還是使用正則來進行判斷。
另外的像post,delete,put就不多說了,其實原理都和get方式的是一樣的。

4.數(shù)據(jù)庫操作

在介紹完路由之后,客戶端就需要在對應的api地址中得到返回,那么路由里面需要執(zhí)行的就是邏輯代碼和進行數(shù)據(jù)庫處理了。并且返回數(shù)據(jù)了。

1)配置文件

首先在根目錄下創(chuàng)建conf文件夾,用來存放數(shù)據(jù)庫密碼等。
這個時候就可以用到cross-env了,還記得我們在package.json中配置了這么一段話么?

 "scripts": {
    "dev": "cross-env NODE_ENV=dev nodemon ./bin/www.js"
  }

這個時候就可以用到這個環(huán)境變量了。通過生產(chǎn)環(huán)境的不同而導出不同的數(shù)據(jù)庫信息,就可以做到在開發(fā)和上線的時候,不用頻繁變更數(shù)據(jù)庫的信息了。

//獲取環(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)建一個db文件夾,用來存放數(shù)據(jù)庫的相關(guān)操作。然后通過npm install mysql來對mysql進行安裝。
導入mysql,導入配置文件,然后創(chuàng)建mysql連接實例con。使用con.connect()進行連接數(shù)據(jù)庫。
通過con.query方法來執(zhí)行sql語句,這里創(chuàng)建了一個通用的方法導出,方便后續(xù)直接使用該方法來執(zhí)行sql語句。
con.query第一個為sql語句,第二個為回調(diào)函數(shù)。

const mysql = require('mysql')

const { MYSQL_CONF } = require('../conf/db')

const con = mysql.createConnection(MYSQL_CONF)

// 開始連接
con.connect()
// 執(zhí)行sql語句
function exec(sql) {
  return new Promise((resolve, reject) => {
    con.query(sql, (error, result) => {
      if (error) {
        console.log(error)
        reject(error)
        return
      }
      resolve(result)
    })
  })
}

當進行完這些步驟的時候,我們就可以正式開始業(yè)務代碼的編寫了,只需要在執(zhí)行完之后返回mysql的exec方法即可。

5.session和redis

session和redis主要是用于登錄模塊,由于有些api需要登錄了之后才能查看,比如單獨用戶的操作,發(fā)文章等等。
session的原理就是在服務器開啟的時候使用一個全局變量,然后將登錄的信息存入全局變量當中,相當于放在了進程的內(nèi)存當中,每次用戶訪問的時候就根據(jù)cookie來看在session中是否有信息,有的話就處于登錄狀態(tài),否則就需要登錄。
弊端:
(1)服務重啟了之后,變量就會消失。
(2)進程內(nèi)存有限,訪問量過大,內(nèi)存會暴增。
(3)上線 之后為多線程,多線程之間內(nèi)存無法共享。

由于存在著以上的弊端,所以則需要使用redis來進行存儲cookie。redis相當于一個獨立的個體,多線程之間也不會出先數(shù)據(jù)無法共存的情況。而且服務重啟的時候,redis還依然在運行。

1)解析cookie

判斷用戶是否登錄除了token之外就是使用cookie了。而且cookie可以是服務端在res中添加,并且返回到客戶端??蛻舳司蜁蟘ookie的信息了。
我們可以通過req.headers.cookie來獲取到客戶端返回的cookie。然后將其轉(zhuǎn)換成對象。

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
})

而在登錄完之后,則需要生成一個userId,然后放在返回的headers當中,這時候客戶端就會有該cookie了

res.setHeader(
  'Set-Cookie',
  `userId=${userId} ; path=/; httpOnly; expires=${getOneDay()}`
)

幾個注意的點:
(1)cookie做限制
需要在服務端res.setHeader(’Set-Cookie‘, 'xxx=xxx')上在最后加一句httpOnly,防止被篡改。
(2)加上時間
在最后加expires= xxx表示有效期截止時間。

2)配置redis

和配置mysql方法差不多,需要在conf文件中配置redis的基本數(shù)據(jù)。然后編寫一個get,set,這里主要用到的只有這兩個方法。再將這兩個方法導出就可以了。
conf文件中redis配置

REDIS_CONF = {
   port: '6379',
   host: '127.0.0.1'
 }

在db文件夾中創(chuàng)建redis文件。創(chuàng)建一個redis實例,監(jiān)聽錯誤的方法,然后導出get和set方法。
注:存儲的是字符串而不是對象

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)存儲redis

當做完配置的工作之后,使用redis大致的步驟就是:
(1)解析cookie。
(2)看cookie中是否存在userId
(3)不存在則創(chuàng)建,存在則去redis中查找是否存在該userId,以此來判斷用戶是否登錄
(4)最后將結(jié)果賦值到req.session中,方便在個人操作時判斷req.session的值來看是否能進行操作

  // 處理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, {})
    // 設置session
    req.session = {}
  } else {
    req.session = result
  }

然后在登錄的時候?qū)eq.session中的userId保存到redis中即可。

const result = await login(username, password)
console.log(result)
if (result[0]) {
  // 設置cookie
  req.session.username = result[0].username
  set(req.sessionId, req.session)
}

6.總結(jié)

在沒有使用express和koa的情況下,主要是為了分析express和koa的底層實現(xiàn)原理,主要是為了更好的理解框架本身,在實踐過程中還是需要用到express和koa的,畢竟為了項目能快速上線,并不是所有都需要從零開始搭建的。
至此大致的主體框架已經(jīng)基本完成了,還剩下的主要就是系統(tǒng)日志的寫入和對錯誤的處理。這一部分通過express和koa的中間件都能很好的進行處理的。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容