日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

這是學習 Node.js 的第七篇,這一篇主要是了解 http,同時實現一個靜態資源服務器。先看一下這個服務器有什么功能。

服務器功能展示

首先我們在命令行工具輸入 ss (意為:super server),它會幫我們在當前目錄啟動一個靜態資源服務器。服務器的地址為 「http://localhost:3000」

手摸手帶你實現一個靜態服務器,超詳細

 

當我們訪問 「http://localhost:3000」時,它把我們當前目錄的所有文件都羅列了出來。

手摸手帶你實現一個靜態服務器,超詳細

 

我們點擊一個文件,例如 pacakge.json,它會把當前文件的內容顯示出來:

手摸手帶你實現一個靜態服務器,超詳細

 

OK,主要功能就是這些,下面我們一起來實現一下。

可以通過 ss --port 3001 指定端口號,通過 ss --directory C:foobar 指定服務器的工作目錄,即靜態資源的根目錄。

http 模塊

既然是服務器,那一定是使用了 Node 的 http 模塊,我們先簡單的了解下如何使用 http 創建一個服務器。

創建一個服務器

const http = require('http')

const server = http.createServer((req, res) => {
  console.log('有請求過來了~~~')
})

let port = 3000
server.listen(port, () => {
  console.log(`server start ${port}`)
})

使用 http.createServer 即可創建一個服務器,然后再調用 server.listen() 方法監聽一個端口,就算正式創建成功了。這時我們直接訪問 http://localhost:3000 即可在命令行看到打印 有請求過來了

那么我們如何獲得這個請求的具體信息,并給客戶端做出相應呢?

其實,每次請求過來的時候都會執行 createServer(callback) 中傳入的回調,回調內會傳入兩個參數:「req(request) 與 res(response)」。req 就代表請求信息與相關操作,res 代表響應信息與相關操作。

我們具體來使用一下這兩個對象。

const http = require('http')
const url = require('url')

const server = http.createServer((req, res) => {
  // 請求方法名
  console.log(req.method)
  // 請求url
  console.log(req.url)
  // 請求頭
  console.log(req.headers)

  // req 是一個可讀流
  req.on('data', chunk => {
    console.log(chunk)
  })
  req.on('end', () => {})

  // 響應行->響應頭->響應體順序不能變
  
  // 首先設置響應行(狀態碼與狀態碼描述)
  res.statusCode = 200
  res.statusMessage = 'success'
  // 設置響應頭
  res.setHeader('name', 'superYue')
  // 最后設置響應體
  // res 是一個可寫流
  res.write('ok')
  res.end('1')
})

let port = 3000
server.listen(port, () => {
  console.log(`server start ${port}`)
})

此時,我們在瀏覽器訪問 http://localhost:3000 就可以下看到如下內容:

手摸手帶你實現一個靜態服務器,超詳細

 

「這里有一些點需要大家注意」

  • req 是一個可讀流,我們獲取請求體的時候,必須要以流的形式獲取,正如我上述代碼所寫的那樣
  • res 是一個可寫流,所以我們設置響應體的時候,必須用 write() 方法,結束響應時,必須用 end() 方法

「對可讀流、可寫流不清楚的同學可以看下此系列文章下的《手寫文件流》」

開始寫代碼

現在我們進入主題——「實現一個靜態資源服務器」

先看一下我們的目錄結構。

手摸手帶你實現一個靜態服務器,超詳細

 

bin 目錄是命令行邏輯代碼

src/satic-server.js 靜態資源服務器

src/template.html html 模板

實現命令行功能

我們的服務器是在命令行內輸入 ss 之后自動啟動的,我們看一下這個功能是怎么實現的?

首先,我們要在 package.json 內增加一個 bin 字段,如下代碼所示:

// pacakge.json
{
  "bin": {
    "ss": "./bin/www.js"
  },
}

ss 是我們運行的命令,./bin/www.js 是運行 ss 后要被執行的 js 文件。

然后,./bin/www.js 內注意要添加這行代碼 #! /usr/bin/env node,這行代碼的意思是用 node 環境來執行以下代碼,這樣我們就可以盡情的去寫 Node 代碼了。

最后,我們要在當前的工作目錄去執行 npm link,這樣才能將 ss 命令注冊到全局變量中去,不然系統是不認識 ss 的。

現在我們已經可以執行 ss 命令了,理論上就可以在 bin/www.js 內去實現一個靜態服務器了,但是在真正實現之前,我想有一些定制化的功能,比如自定義啟動服務的端口號,自定義靜態服務器的工作目錄。

要實現這樣的定制化功能,那肯定是在命令行內去輸入,例如:

ss --port 3000 啟動一個 3000 端口的服務器

ss --directory C: 靜態資源服務器的根目錄是 C 盤。

然后我們要解析 ss 輸入的參數,這些參數 Node 都幫我們保存在了 process.argv 屬性里,打印出來的結果如下圖所示。

手摸手帶你實現一個靜態服務器,超詳細

 

如果我們想得到正確的結果,需要我們自己去解析。這里給大家推薦一個工具——commander,它是一個完整的 node.js 命令行解決方案,github鏈接點這。

我們來看一下示例:

const { program } = require('commander)

// 聲明一個 prot 參數,要求必須有值,默認值是 3000
// 'set your server port' 是命令描述
program.option('-p, --port <v>', 'set your server port', 3000);
// 開始解析命令
program.parse(process.argv);
// 通過 program.port 拿到解析好參數
console.log(`port: ${program.port}`);

可以看到,最終我們輸入的命令都會被解析到 program 內。

這只是 commander 一部分功能,完整功能可以看具體文檔。

接下來把我 www.js 代碼貼出來

#! /usr/bin/env node

const program = require('commander')
const StaticServer = require('../src/static-server')
console.log(process.argv)
program.name('ss')

program
  .option('-p, --port <v>', 'set your server port', 3000)
  .option('-d, --directory <v>', 'set your server start directory', process.cwd())

program.on('--help', () => {
  console.log('nExamples:')
  console.log('ss -p 3000 / ss --port 3000')
  console.log('ss -d C: / ss --directory C:')
})

program.parse(process.argv)

const config = {}
config.port = program.port ?? process.cwd()

new StaticServer(config).start()

program.on('--help') 的意思是監聽 --help 命令。每當用戶輸入 ss --help 的時候,我們都把操作提示給打印出來。

靜態資源服務器

在上段代碼內,我們在 www.js 里執行了 new StaticServer(config).start(),這句代碼的意思是啟動一個靜態資源服務器,接下來,我們就來實現一下這個。

初始化參數

首先,我們聲明一個類,并初始化參數。

class StaticServer {
  constructor(config) {
    this.port = config.port
    this.directory = config.directory
  }
}

start()

然后,在調用 start 的時候,我們創建一個服務器。

start() {
  const server = http.createServer(this.handleRequest.bind(this))
  server.listen(this.port, () => {
    console.log(`${chalk.yellow('Starting up super-server: ')}${this.directory}`)
    console.log(`http://localhost:${chalk.green(this.port)}`)
  })
}

為了更好的處理請求,我們把處理請求的邏輯全都放到了 handleRequest() 方法內。

chalk 中文意思為粉筆,是專門用來改變控制臺輸出顏色的第三方包。

handleRequest()

這個方法是專門用來處理請求的。

我們現在想一下,當一個靜態資源的請求過來時,我們應該做什么操作?

  1. 判斷請求的資源是一個文件還是一個文件夾。如果是文件,則直接返回文件內容;如果是文件夾,則返回文件夾內存在的資源列表。
  2. 對文件資源進行緩存。因為是靜態資源服務器,所以肯定要有緩存功能,因為文件是不會經常變的。
  3. 響應文件內容。

我們看下具體代碼

async handleRequest(req, res) {
  // 獲取請求路徑
  // url 為 Node 的核心模塊
  const { pathname } = url.parse(req.url)
  // 工作目錄與請求路徑拼接,得到最終的靜態資源地址
  // 這里的工作目錄默認是 process.cwd(),意思是當前代碼啟動的目錄
  // 可以通過 --directory 去指定
  const filePath = path.join(this.directory, pathname)

  try {
    // 獲取文件信息
    const stat = await fs.stat(filePath)

    if (stat.isFile()) {
      // 如果是文件,則返回文件信息
      this.sendFile(req, res, filePath, stat)
    } else {
      // 如果是文件夾,則返回資源列表
      this.sendFolder(req, res, filePath, pathname)
    }
  } catch(e) {
    // 返回錯誤信息
    this.sendError(req, res, e)
  }
}

代碼注釋非常詳細,相信不用做過多的解釋。

這里最終獲取靜態資源的地址是:請求的路徑 + 服務器工作目錄(默認是 process.cwd(),可以通過 --dircetory 去指定

sendFile()

sendFile 對客戶端響應文件信息,在響應之前,要做緩存相關的操作,這些操作都放在了 cache() 方法內。

緩存包括強緩存與協商緩存,強緩存取的是瀏覽器客戶端內的內容,瀏覽器不會對服務器發起響應。協商緩存需要服務器判斷文件是否發生了變化,如果未發生變化則返回 304。

在具體返回響應之前,要設置響應內容的 mime 格式,用來告訴客戶端如何處理這段內容。例如,如果是 html 內容,那我們的 Content-Type 響應頭必須是 text/html,不然瀏覽器不能正確的解析。這里我們使用了 mime 這個第三方包,它可以根據文件后綴得到正確的 mime 類型。

響應內容的時候,我們會以流的形式去響應,所以這里我們創建了一個文件可讀流。

sendFile(req, res, filePath, stat) {
  if (this.cache(req, res, filePath, stat)) {
    res.statusCode = 304
    res.end()
    return;
  }

  res.setHeader('Content-Type', mime.getType(filePath))
  createReadStream(filePath).pipe(res)
}

sendFolder()

返回文件夾內的文件列表。

將文件夾內的文件全部讀取出來,并以 html 的形式返回給瀏覽器以供展示。這里使用了 ejs 模板引擎來渲染 html。

async sendFolder(req, res, filePath, pathname) {
  let dirs = await fs.readdir(filePath)
  dirs = dirs.map(item => ({
    filename: item,
    href: path.join(pathname, item)
  }))
  console.log(dirs)
  const template = await fs.readFile(path.resolve(__dirname, './template.html'), 'utf-8')

  const html = await ejs.render(template, { dirs }, { async: true })

  res.setHeader('Content-Type', 'text/html;charset=utf-8')
  res.end(html)
}

我們將讀取出來的文件列表傳給 template 靜態模板,然后利用 ejs 的得到渲染后的 html。

template.html 模板代碼如下

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>zs</title>
</head>
<body>

<!-- 出路路徑 盡量不要采用./ ../  絕對路徑 /a/a.js -->

  <%dirs.forEach(item=>{%>
      <li><a href="<%=item.href%>"><%=item.filename%></a></li>
  <%})%>
</body>
</html>

cache()

cache() 方法封裝了文件緩存操作。

首先對文件應用緩存,設置 Expires 與 Cache-Control 響應頭,這兩個字段設置任何一個字段都可以實現緩存,但為了最大的保證兼容性,我們這里都做了設置。

如果瀏覽器緩存失效,會重新發起請求,這時需要服務器判斷資源是否真的被更改了,判斷文件資源的緩存是否失效有兩種方案。

  1. 通過判斷文件的修改時間來確定是否失效。第一次請求時,將文件的修改時間通過 Last-Modified 響應頭帶給瀏覽器,瀏覽器下次請求時會將修改時間放入 if-modified-since 請求頭內,這時我們就可以通過 if-modified-since 字段與當前文件的修改時間做比較,來判斷文件是否被修改了。

但是這種做法有缺陷,假如我們將文件修改了,然后過一會又修改成原來的內容,這時最終的文件是沒有變化的,但是文件的修改時間卻變了,這樣就導致緩存失效。

  1. 第二種解決了上述問題。第一次請求時,返回給瀏覽器的是一個文件摘要,以后請求時,根據文件摘要來判斷資源是否過期。由于文件摘要是根據文件內容生成的,所以文件內容不變,摘要就不會變。用來控制緩存的字段分別是,瀏覽器:ifNoneMatch;客戶端:Etag。
cache(req, res, filePath, stat) {
  res.setHeader('Expires', new Date(Date.now() + 10 * 1000).toGMTString())
  res.setHeader('Cache-Control', `max-age=${10}`)

  const ifModifiedSince = req.headers['if-modified-since']
  const ctime = stat.ctime.toGMTString()
  if (ifModifiedSince !== ctime) {
    return false
  }

  const ifNoneMatch = req.headers['if-none-match']
  // 利用 MD5 生成文件摘要
  // crypto 為內置的加密算法
  const etag = crypto.createHash('md5').update( readFileSync(filePath)).digest('base64')
  if (ifNoneMatch !== etag) {
    return false
  }

  res.setHeader('Last-Modified', ctime)
  res.setHeader('Etag', etag)

  return true;
}

完整代碼

const http = require('http')
const url = require('url')
const fs = require('fs').promises
const path = require('path')
const { createReadStream, readFileSync } = require('fs')
const crypto = require('crypto')

const chalk = require('chalk')
const mime = require('mime')
const ejs = require('ejs')

class StaticServer {
  constructor(config) {
    this.port = config.port
    this.directory = config.directory
  }

  start() {
    const server = http.createServer(this.handleRequest.bind(this))
    server.listen(this.port, () => {
      console.log(`${chalk.yellow('Starting up super-server: ')}${this.directory}`)
      console.log(`http://localhost:${chalk.green(this.port)}`)
    })
  }

  async handleRequest(req, res) {
    const { pathname } = url.parse(req.url)
    const filePath = path.join(this.directory, pathname)
    console.log(filePath)

    try {
      const stat = await fs.stat(filePath)
      
      if (stat.isFile()) {
        this.sendFile(req, res, filePath, stat)
      } else {
        this.sendFolder(req, res, filePath, pathname)
      }
    } catch(e) {
      this.sendError(req, res, e)
    }
  }

  cache(req, res, filePath, stat) {
    res.setHeader('Expires', new Date(Date.now() + 10 * 1000).toGMTString())
    res.setHeader('Cache-Control', `max-age=${10}`)

    const ifModifiedSince = req.headers['if-modified-since']
    const ctime = stat.ctime.toGMTString()
    if (ifModifiedSince === ctime) {
      return true
    }

    const ifNoneMatch = req.headers['if-none-match']
    const etag = crypto.createHash('md5').update( readFileSync(filePath)).digest('base64')
    if (ifNoneMatch === etag) {
      return true
    }

    res.setHeader('Last-Modified', ctime)
    res.setHeader('Etag', etag)

    return false;
  }

  sendFile(req, res, filePath, stat) {
    if (this.cache(req, res, filePath, stat)) {
      res.statusCode = 304
      res.end()
      return;
    }

    res.setHeader('Content-Type', mime.getType(filePath))
    createReadStream(filePath).pipe(res)
  }

  async sendFolder(req, res, filePath, pathname) {
    let dirs = await fs.readdir(filePath)
    dirs = dirs.map(item => ({
      filename: item,
      href: path.join(pathname, item)
    }))
    console.log(dirs)
    const template = await fs.readFile(path.resolve(__dirname, './template.html'), 'utf-8')

    const html = await ejs.render(template, { dirs }, { async: true })

    res.setHeader('Content-Type', 'text/html;charset=utf-8')
    res.end(html)
  }

  sendError(req, res, e) {
    res.end(e.message)
  }
}

module.exports = StaticServer

總結

可以看到,這個靜態資源服務器并不是特別復雜,但是它卻給我們帶來了不少知識點。

  • 命令行工具的使用
  • 一個 http 請求與響應的具體過程
  • http 緩存策略
  • http、url、crypto 等核心模塊的使用

希望這篇文章可以給大家帶來一些收獲~~~

也可以看下這一系列的其它文章~~~

分享到:
標簽:靜態 服務器
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定