成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

koa源碼閱讀[3]-koa-send與它的衍生(static)

jimhs / 2232人閱讀

摘要:源碼閱讀的第四篇,涉及到向接口請(qǐng)求方提供文件數(shù)據(jù)。是一個(gè)基于的淺封裝。小結(jié)與算是兩個(gè)非常輕量級(jí)的中間件了。

koa源碼閱讀的第四篇,涉及到向接口請(qǐng)求方提供文件數(shù)據(jù)。

第一篇:koa源碼閱讀-0  
第二篇:koa源碼閱讀-1-koa與koa-compose
第三篇:koa源碼閱讀-2-koa-router

處理靜態(tài)文件是一個(gè)繁瑣的事情,因?yàn)殪o態(tài)文件都是來自于服務(wù)器上,肯定不能放開所有權(quán)限讓接口來讀取。
各種路徑的校驗(yàn),權(quán)限的匹配,都是需要考慮到的地方。
koa-sendkoa-static就是幫助我們處理這些繁瑣事情的中間件。
koa-sendkoa-static的基礎(chǔ),可以在NPM的界面上看到,staticdependencies中包含了koa-send

koa-send主要是用于更方便的處理靜態(tài)文件,與koa-router之類的中間件不同的是,它并不是直接作為一個(gè)函數(shù)注入到app.use中的。
而是在某些中間件中進(jìn)行調(diào)用,傳入當(dāng)前請(qǐng)求的Context及文件對(duì)應(yīng)的位置,然后實(shí)現(xiàn)功能。

koa-send的GitHub地址

原生的文件讀取、傳輸方式

Node中,如果使用原生的fs模塊進(jìn)行文件數(shù)據(jù)傳輸,大致是這樣的操作:

const fs      = require("fs")
const Koa     = require("koa")
const Router  = require("koa-router")

const app     = new Koa()
const router  = new Router()
const file    = "./test.log"
const port    = 12306

router.get("/log", ctx => {
  const data = fs.readFileSync(file).toString()
  ctx.body = data
})

app.use(router.routes())
app.listen(port, () => console.log(`Server run as http://127.0.0.1:${port}`))

或者用createReadStream代替readFileSync也是可行的,區(qū)別會(huì)在下邊提到

這個(gè)簡(jiǎn)單的示例僅針對(duì)一個(gè)文件進(jìn)行操作,而如果我們要讀取的文件是有很多個(gè),甚至于可能是通過接口參數(shù)傳遞過來的。
所以很難保證這個(gè)文件一定是真實(shí)存在的,而且我們可能還需要添加一些權(quán)限設(shè)置,防止一些敏感文件被接口返回。

router.get("/file", ctx => {
  const { fileName } = ctx.query
  const path = path.resolve("./XXX", fileName)
  // 過濾隱藏文件
  if (path.startsWith(".")) {
    ctx.status = 404
    return
  }

  // 判斷文件是否存在
  if (!fs.existsSync(path)) {
    ctx.status = 404
    return
  }

  // balabala

  const rs = fs.createReadStream(path)
  ctx.body = rs // koa做了針對(duì)stream類型的處理,詳情可以看之前的koa篇
})

添加了各種邏輯判斷以后,讀取靜態(tài)文件就變得安全不少,可是這也只是在一個(gè)router中做的處理。
如果有多個(gè)接口都會(huì)進(jìn)行靜態(tài)文件的讀取,勢(shì)必會(huì)存在大量的重復(fù)邏輯,所以將其提煉為一個(gè)公共函數(shù)將是一個(gè)很好的選擇。

koa-send的方式

這就是koa-send做的事情了,提供了一個(gè)封裝非常完善的處理靜態(tài)文件的中間件。
這里是兩個(gè)最基礎(chǔ)的使用例子:

const path = require("path")
const send = require("koa-send")

// 針對(duì)某個(gè)路徑下的文件獲取
router.get("/file", async ctx => {
  await send(ctx, ctx.query.path, {
    root: path.resolve(__dirname, "./public")
  })
})

// 針對(duì)某個(gè)文件的獲取
router.get("/index", async ctx => {
  await send(ctx, "./public/index.log")
})

假設(shè)我們的目錄結(jié)構(gòu)是這樣的,simple-send.js為執(zhí)行文件:

.
├── public
│?? ├── a.log
│?? ├── b.log
│?? └── index.log
└── simple-send.js

使用/file?path=XXX就可以很輕易的訪問到public下的文件。
以及訪問/index就可以拿到/public/index.log文件的內(nèi)容。

koa-send提供的功能

koa-send提供了很多便民的選項(xiàng),除去常用的root以外,還有大概小十個(gè)的選項(xiàng)可供使用:

options type default desc
maxage Number 0 設(shè)置瀏覽器可以緩存的毫秒數(shù)
對(duì)應(yīng)的Header: Cache-Control: max-age=XXX
immutable Boolean false 通知瀏覽器該URL對(duì)應(yīng)的資源不可變,可以無限期的緩存
對(duì)應(yīng)的Header: Cache-Control: max-age=XXX, immutable
hidden Boolean false 是否支持隱藏文件的讀取
.開頭的文件被稱為隱藏文件
root String - 設(shè)置靜態(tài)文件路徑的根目錄,任何該目錄之外的文件都是禁止訪問的。
index String - 設(shè)置一個(gè)默認(rèn)的文件名,在訪問目錄的時(shí)候生效,會(huì)自動(dòng)拼接到路徑后邊 (此處有一個(gè)小彩蛋)
gzip Boolean true 如果訪問接口的客戶端支持gzip,并且存在.gz后綴的同名文件的情況下會(huì)傳遞.gz文件
brotli Boolean true 邏輯同上,如果支持brotli且存在.br后綴的同名文件
format Boolean true 開啟以后不會(huì)強(qiáng)要求路徑結(jié)尾的/,/path/path/表示的是一個(gè)路徑 (僅在path是一個(gè)目錄的情況下生效)
extensions Array false 如果傳遞了一個(gè)數(shù)組,會(huì)嘗試將數(shù)組中的所有item作為文件的后綴進(jìn)行匹配,匹配到哪個(gè)就讀取哪個(gè)文件
setHeaders Function - 用來手動(dòng)指定一些Headers,意義不大
參數(shù)們的具體表現(xiàn)

有些參數(shù)的搭配可以實(shí)現(xiàn)一些神奇的效果,有一些參數(shù)會(huì)影響到Header,也有一些參數(shù)是用來優(yōu)化性能的,類似gzipbrotli的選項(xiàng)。

koa-send的主要邏輯可以分為這幾塊:

path路徑有效性的檢查

gzip等壓縮邏輯的應(yīng)用

文件后綴、默認(rèn)入口文件的匹配

讀取文件數(shù)據(jù)

在函數(shù)的開頭部分有這樣的邏輯:

const resolvePath = require("resolve-path")
const {
  parse
} = require("path")

async function send (ctx, path. opts = {}) {
  const trailingSlash = path[path.length - 1] === "/"
  const index = opts.index

  // 此處省略各種參數(shù)的初始值設(shè)置

  path = path.substr(parse(path).root.length)

  // ...

  // normalize path
  path = decode(path) // 內(nèi)部調(diào)用的是`decodeURIComponent`
  // 也就是說傳入一個(gè)轉(zhuǎn)義的路徑也是可以正常使用的

  if (index && trailingSlash) path += index

  path = resolvePath(root, path)

  // hidden file support, ignore
  if (!hidden && isHidden(root, path)) return
}

function isHidden (root, path) {
  path = path.substr(root.length).split(sep)
  for (let i = 0; i < path.length; i++) {
    if (path[i][0] === ".") return true
  }
  return false
}
路徑檢查

首先是判斷傳入的path是否為一個(gè)目錄,_(結(jié)尾為/會(huì)被認(rèn)為是一個(gè)目錄)_。
如果是目錄,并且存在一個(gè)有效的index參數(shù),則會(huì)將index拼接到path后邊。
也就是大概這樣的操作:

send(ctx, "./public/", {
  index: "index.js"
})

// ./public/index.js

resolve-path 是一個(gè)用來處理路徑的包,用來幫助過濾一些異常的路徑,類似path//file、/etc/XXX 這樣的惡意路徑,并且會(huì)返回處理后絕對(duì)路徑。

isHidden用來判斷是否需要過濾隱藏文件。
因?yàn)榈彩?b>.開頭的文件都會(huì)被認(rèn)為隱藏文件,同理目錄使用.開頭也會(huì)被認(rèn)為是隱藏的,所以就有了isHidden函數(shù)的實(shí)現(xiàn)。

其實(shí)我個(gè)人覺得這個(gè)使用一個(gè)正則就可以解決的問題。。為什么還要分割為數(shù)組呢?

function isHidden (root, path) {
  path = path.substr(root.length)

  return new RegExp(`${sep}.`).test(path)
}

已經(jīng)給社區(qū)提交了PR。

壓縮的開啟與文件夾的處理

在上邊的這一坨代碼執(zhí)行完以后,我們就得到了一個(gè)有效的路徑,_(如果是無效路徑,resolvePath會(huì)直接拋出異常)_
接下來做的事情就是檢查是否有可用的壓縮文件使用,此處沒有什么邏輯,就是簡(jiǎn)單的exists操作,以及Content-Encoding的修改 _(用于開啟壓縮)_。

后綴的匹配:

if (extensions && !/.[^/]*$/.exec(path)) {
  const list = [].concat(extensions)
  for (let i = 0; i < list.length; i++) {
    let ext = list[i]
    if (typeof ext !== "string") {
      throw new TypeError("option extensions must be array of strings or false")
    }
    if (!/^./.exec(ext)) ext = "." + ext
    if (await fs.exists(path + ext)) {
      path = path + ext
      break
    }
  }
}

可以看到這里的遍歷是完全按照我們調(diào)用send是傳入的順序來走的,并且還做了.符號(hào)的兼容。
也就是說這樣的調(diào)用都是有效的:

await send(ctx, "path", {
  extensions: [".js", "ts", ".tsx"]
})

如果在添加了后綴以后能夠匹配到真實(shí)的文件,那么就認(rèn)為這是一個(gè)有效的路徑,然后進(jìn)行了break的操作,也就是文檔中所說的:First found is served.。

在結(jié)束這部分操作以后會(huì)進(jìn)行目錄的檢測(cè),判斷當(dāng)前路徑是否為一個(gè)目錄:

let stats
try {
  stats = await fs.stat(path)

  if (stats.isDirectory()) {
    if (format && index) {
      path += "/" + index
      stats = await fs.stat(path)
    } else {
      return
    }
  }
} catch (err) {
  const notfound = ["ENOENT", "ENAMETOOLONG", "ENOTDIR"]
  if (notfound.includes(err.code)) {
    throw createError(404, err)
  }
  err.status = 500
  throw err
}
一個(gè)小彩蛋

可以發(fā)現(xiàn)一個(gè)很有意思的事情,如果發(fā)現(xiàn)當(dāng)前路徑是一個(gè)目錄以后,并且明確指定了format,那么還會(huì)再嘗試拼接一次index。
這就是上邊所說的那個(gè)彩蛋了,當(dāng)我們的public路徑結(jié)構(gòu)長(zhǎng)得像這樣的時(shí)候:

└── public
 ?? └── index
 ??  ?? └── index # 實(shí)際的文件 hello

我們可以通過一個(gè)簡(jiǎn)單的方式獲取到最底層的文件數(shù)據(jù):

router.get("/surprises", async ctx => {
  await send(ctx, "/", {
    root: "./public",
    index: "index"
  })
})

// > curl http://127.0.0.1:12306/surprises
// hello

這里就用到了上邊的幾個(gè)邏輯處理,首先是trailingSlash的判斷,如果以/結(jié)尾會(huì)拼接index,以及如果當(dāng)前path匹配為是一個(gè)目錄以后,又會(huì)拼接一次index。
所以一個(gè)簡(jiǎn)單的/加上index的參數(shù)就可以直接獲取到/index/index。
一個(gè)小小的彩蛋,實(shí)際開發(fā)中應(yīng)該很少會(huì)這么玩

最終的讀取文件操作

最后終于來到了文件讀取的邏輯處理,首先就是調(diào)用setHeaders的操作。

因?yàn)榻?jīng)過上邊的層層篩選,這里拿到的path和你調(diào)用send時(shí)傳入的path不是同一個(gè)路徑。
不過倒也沒有必要必須在setHeaders函數(shù)中進(jìn)行處理,因?yàn)榭梢钥吹皆诤瘮?shù)結(jié)束時(shí),將實(shí)際的path返回了出來。
我們完全可以在send執(zhí)行完畢后再進(jìn)行設(shè)置,至于官方readme中所寫的and doing it after is too late because the headers are already sent.。
這個(gè)不需要擔(dān)心,因?yàn)?b>koa的返回?cái)?shù)據(jù)都是放到ctx.body中的,而body的解析是在所有的中間件全部執(zhí)行完以后才會(huì)進(jìn)行處理。
也就是說所有的中間件都執(zhí)行完以后才會(huì)開始發(fā)送http請(qǐng)求體,在此之前設(shè)置Header都是有效的。

if (setHeaders) setHeaders(ctx.res, path, stats)

// stream
ctx.set("Content-Length", stats.size)
if (!ctx.response.get("Last-Modified")) ctx.set("Last-Modified", stats.mtime.toUTCString())
if (!ctx.response.get("Cache-Control")) {
  const directives = ["max-age=" + (maxage / 1000 | 0)]
  if (immutable) {
    directives.push("immutable")
  }
  ctx.set("Cache-Control", directives.join(","))
}
if (!ctx.type) ctx.type = type(path, encodingExt) // 接口返回的數(shù)據(jù)類型,默認(rèn)會(huì)取出文件后綴
ctx.body = fs.createReadStream(path)

return path

以及包括上邊的maxageimmutable都是在這里生效的,但是要注意的是,如果Cache-Control已經(jīng)存在值了,koa-send是不會(huì)去覆蓋的。

使用Stream與使用readFile的區(qū)別

在最后給body賦值的位置可以看到,是使用的Stream而并非是readFile,使用Stream進(jìn)行傳輸能帶來至少兩個(gè)好處:

第一種方式,如果是大文件,在讀取完成后會(huì)臨時(shí)存放到內(nèi)存中,并且toString是有長(zhǎng)度限制的,如果是一個(gè)巨大的文件,toString調(diào)用會(huì)拋出異常的。

采用第一種方式進(jìn)行讀取文件,是要在全部的數(shù)據(jù)都讀取完成后再返回給接口調(diào)用方,在讀取數(shù)據(jù)的期間,接口都是處于Wait的狀態(tài),沒有任何數(shù)據(jù)返回。

可以做一個(gè)類似這樣的Demo:

const http      = require("http")
const fs        = require("fs")
const filePath  = "./test.log"
  
http.createServer((req, res) => {
  if (req.url === "/") {
    res.end("")
  } else if (req.url === "/sync") {
    const data = fs.readFileSync(filePath).toString()

    res.end(data)
  } else if (req.url === "/pipe") {
    const rs = fs.createReadStream(filePath)

    rs.pipe(res)
  } else {
    res.end("404")
  }
}).listen(12306, () => console.log("server run as http://127.0.0.1:12306"))

首先訪問首頁(yè)http://127.0.0.1:12306/進(jìn)入一個(gè)空的頁(yè)面 _(主要是懶得搞CORS了)_,然后在控制臺(tái)調(diào)用兩個(gè)fetch就可以得到這樣的對(duì)比結(jié)果了:


可以看出在下行傳輸?shù)臅r(shí)間相差無幾的同時(shí),使用readFileSync的方式會(huì)增加一定時(shí)間的Waiting,而這個(gè)時(shí)間就是服務(wù)器在進(jìn)行文件的讀取,時(shí)間長(zhǎng)短取決于讀取的文件大小,以及機(jī)器的性能。

koa-static

koa-static是一個(gè)基于koa-send的淺封裝。
因?yàn)橥ㄟ^上邊的實(shí)例也可以看到,send方法需要自己在中間件中調(diào)用才行。
手動(dòng)指定send對(duì)應(yīng)的path之類的參數(shù),這些也是屬于重復(fù)性的操作,所以koa-static將這些邏輯進(jìn)行了一次封裝。
讓我們可以通過直接注冊(cè)一個(gè)中間件來完成靜態(tài)文件的處理,而不再需要關(guān)心參數(shù)的讀取之類的問題:

const Koa = require("koa")
const app = new Koa()
app.use(require("koa-static")(root, opts))

opts是透?jìng)鞯?b>koa-send中的,只不過會(huì)使用第一個(gè)參數(shù)root來覆蓋opts中的root。
并且添加了一些細(xì)節(jié)化的操作:

默認(rèn)添加一個(gè)index.html

if (opts.index !== false) opts.index = opts.index || "index.html"

默認(rèn)只針對(duì)HEADGET兩種METHOD

if (ctx.method === "HEAD" || ctx.method === "GET") {
  // ...
}

添加一個(gè)defer選項(xiàng)來決定是否先執(zhí)行其他中間件。

如果deferfalse,則會(huì)先執(zhí)行send,優(yōu)先匹配靜態(tài)文件。
否則則會(huì)等到其余中間件先執(zhí)行,確定其他中間件沒有處理該請(qǐng)求才會(huì)去尋找對(duì)應(yīng)的靜態(tài)資源。
只需指定root,剩下的工作交給koa-static,我們就無需關(guān)心靜態(tài)資源應(yīng)該如何處理了。

小結(jié)

koa-sendkoa-static算是兩個(gè)非常輕量級(jí)的中間件了。
本身沒有太復(fù)雜的邏輯,就是一些重復(fù)的邏輯被提煉成的中間件。
不過確實(shí)能夠減少很多日常開發(fā)中的任務(wù)量,可以讓人更專注的關(guān)注業(yè)務(wù),而非這些邊邊角角的功能。

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/97001.html

相關(guān)文章

  • koa-static中間件學(xué)習(xí)搭建靜態(tài)文件服務(wù)器

    摘要:從中間件學(xué)習(xí)搭建靜態(tài)文件服務(wù)器原文地址中有說明它只是的一個(gè)包裝查看的源碼可以發(fā)現(xiàn),它做的工作是根據(jù)傳入的查找文件是否存在,如果存在就創(chuàng)建一個(gè)流,不存在就拋出錯(cuò)誤。 從koa-static中間件學(xué)習(xí)搭建靜態(tài)文件服務(wù)器 原文地址 koa-send Static file serving middleware koa-static中有說明它只是koa-send的一個(gè)包裝 const send...

    Olivia 評(píng)論0 收藏0
  • Koa-Static 該換換了吧 試試 Awesome-Static

    摘要:吼,所以我做了,和一樣,都是對(duì)的一層封裝,只不過用編寫,對(duì)支持良好。 其實(shí)還是得按自個(gè)兒的需求來。 koa-static 有啥問題么 koa-static是一個(gè)非常輕量的koa中間件,能夠迅速的搭建起一個(gè)靜態(tài)文件服務(wù)器,通常我們把靜態(tài)文件都放進(jìn)public,并且通過類似koa-static這樣的東西來將我們的public作為靜態(tài)目錄,這樣的話,我們就能直接通過根路由進(jìn)行訪問了。 emm...

    kevin 評(píng)論0 收藏0
  • Koa-Static 該換換了吧 試試 Awesome-Static

    摘要:吼,所以我做了,和一樣,都是對(duì)的一層封裝,只不過用編寫,對(duì)支持良好。 其實(shí)還是得按自個(gè)兒的需求來。 koa-static 有啥問題么 koa-static是一個(gè)非常輕量的koa中間件,能夠迅速的搭建起一個(gè)靜態(tài)文件服務(wù)器,通常我們把靜態(tài)文件都放進(jìn)public,并且通過類似koa-static這樣的東西來將我們的public作為靜態(tài)目錄,這樣的話,我們就能直接通過根路由進(jìn)行訪問了。 emm...

    CNZPH 評(píng)論0 收藏0
  • Koa源碼閱讀筆記(4) -- ctx對(duì)象

    摘要:本筆記共四篇源碼閱讀筆記源碼閱讀筆記源碼閱讀筆記服務(wù)器啟動(dòng)與請(qǐng)求處理源碼閱讀筆記對(duì)象起因前兩天終于把自己一直想讀的源代碼讀了一遍。首先放上關(guān)鍵的源代碼在上一篇源碼閱讀筆記服務(wù)器啟動(dòng)與請(qǐng)求處理中,我們已經(jīng)分析了的作用。 本筆記共四篇Koa源碼閱讀筆記(1) -- coKoa源碼閱讀筆記(2) -- composeKoa源碼閱讀筆記(3) -- 服務(wù)器の啟動(dòng)與請(qǐng)求處理Koa源碼閱讀筆記(4...

    ityouknow 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

jimhs

|高級(jí)講師

TA的文章

閱讀更多
最新活動(dòng)
閱讀需要支付1元查看
<