<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      手寫koa-static源碼,深入理解靜態服務器原理

      這篇文章繼續前面的Koa源碼系列,這個系列已經有兩篇文章了:

      1. 第一篇講解了Koa的核心架構和源碼:手寫Koa.js源碼
      2. 第二篇講解了@koa/router的架構和源碼:手寫@koa/router源碼

      本文會接著講一個常用的中間件----koa-static,這個中間件是用來搭建靜態服務器的。

      其實在我之前使用Node.js原生API寫一個web服務器已經講過怎么返回一個靜態文件了,代碼雖然比較丑,基本流程還是差不多的:

      1. 通過請求路徑取出正確的文件地址
      2. 通過地址獲取對應的文件
      3. 使用Node.js的API返回對應的文件,并設置相應的header

      koa-static的代碼更通用,更優雅,而且對大文件有更好的支持,下面我們來看看他是怎么做的吧。本文還是采用一貫套路,先看一下他的基本用法,然后從基本用法入手去讀源碼,并手寫一個簡化版的源碼來替換他。

      本文可運行代碼已經上傳GitHub,大家可以拿下來玩玩:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Node.js/KoaStatic

      基本用法

      koa-static使用很簡單,主要代碼就一行:

      const Koa = require('koa');
      const serve = require('koa-static');
      
      const app = new Koa();
      
      // 主要就是這行代碼
      app.use(serve('public'));
      
      app.listen(3001, () => {
          console.log('listening on port 3001');
      });
      

      上述代碼中的serve就是koa-static,他運行后會返回一個Koa中間件,然后Koa的實例直接引用這個中間件就行了。

      serve方法支持兩個參數,第一個是靜態文件的目錄,第二個參數是一些配置項,可以不傳。像上面的代碼serve('public')就表示public文件夾下面的文件都可以被外部訪問。比如我在里面放了一張圖片:

      image-20201125163558774

      跑起來就是這樣子:

      image.png

      注意上面這個路徑請求的是/test.jpg,前面并沒有public,說明koa-static對請求路徑進行了判斷,發現是文件就映射到服務器的public目錄下面,這樣可以防止外部使用者探知服務器目錄結構。

      手寫源碼

      返回的是一個Koa中間件

      我們看到koa-static導出的是一個方法serve,這個方法運行后返回的應該是一個Koa中間件,這樣Koa才能引用他,所以我們先來寫一下這個結構吧:

      module.exports = serve;   // 導出的是serve方法
      
      // serve接受兩個參數
      // 第一個參數是路徑地址
      // 第二個是配置選項
      function serve(root, opts) {
          // 返回一個方法,這個方法符合koa中間件的定義
          return async function serve(ctx, next) {
              await next();
          }
      }
      

      調用koa-send返回文件

      現在這個中間件是空的,其實他應該做的是將文件返回,返回文件的功能也被單獨抽取出來成了一個庫----koa-send,我們后面會看他源碼,這里先直接用吧。

      function serve(root, opts) {
          // 這行代碼如果效果就是
          // 如果沒傳opts,opts就是空對象{}
          // 同時將它的原型置為null
          opts = Object.assign(Object.create(null), opts);
      
          // 將root解析為一個合法路徑,并放到opts上去
          // 因為koa-send接收的路徑是在opts上
          opts.root = resolve(root);
        
        	// 這個是用來兼容文件夾的,如果請求路徑是一個文件夾,默認去取index
          // 如果用戶沒有配置index,默認index就是index.html
          if (opts.index !== false) opts.index = opts.index || 'index.html';
      
        	// 整個serve方法的返回值是一個koa中間件
        	// 符合koa中間件的范式: (ctx, next) => {}
          return async function serve(ctx, next) {
              let done = false;    // 這個變量標記文件是否成功返回
      
              // 只有HEAD和GET請求才響應
              if (ctx.method === 'HEAD' || ctx.method === 'GET') {
                  try {
                      // 調用koa-send發送文件
                      // 如果發送成功,koa-send會返回路徑,賦值給done
                      // done轉換為bool值就是true
                      done = await send(ctx, ctx.path, opts);
                  } catch (err) {
                      // 如果不是404,可能是一些400,500這種非預期的錯誤,將它拋出去
                      if (err.status !== 404) {
                          throw err
                      }
                  }
              }
      
              // 通過done來檢測文件是否發送成功
              // 如果沒成功,就讓后續中間件繼續處理他
              // 如果成功了,本次請求就到此為止了
              if (!done) {
                  await next()
              }
          }
      }
      

      opt.defer

      defer是配置選項opt里面的一個可選參數,他稍微特殊一點,默認為false,如果你傳了truekoa-static會讓其他中間件先響應,即使其他中間件寫在koa-static后面也會讓他先響應,自己最后響應。要實現這個,其實就是控制調用next()的時機。在講Koa源碼的文章里面已經講過了,調用next()其實就是在調用后面的中間件,所以像上面代碼那樣最后調用next(),就是先執行koa-static然后再執行其他中間件。如果你給defer傳了true,其實就是先執行next(),然后再執行koa-static的邏輯,按照這個思路我們來支持下defer吧:

      function serve(root, opts) {
          opts = Object.assign(Object.create(null), opts);
      
          opts.root = resolve(root);
      
          // 如果defer為false,就用之前的邏輯,最后調用next
          if (!opts.defer) {
              return async function serve(ctx, next) {
                  let done = false;    
      
                  if (ctx.method === 'HEAD' || ctx.method === 'GET') {
                      try {
                          done = await send(ctx, ctx.path, opts);
                      } catch (err) {
                          if (err.status !== 404) {
                              throw err
                          }
                      }
                  }
      
                  if (!done) {
                      await next()
                  }
              }
          }
      
          // 如果defer為true,先調用next,然后執行自己的邏輯
          return async function serve(ctx, next) {
              // 先調用next,執行后面的中間件
              await next();
      
              if (ctx.method !== 'HEAD' && ctx.method !== 'GET') return
      
              // 如果ctx.body有值了,或者status不是404,說明請求已經被其他中間件處理過了,就直接返回了
              if (ctx.body != null || ctx.status !== 404) return // eslint-disable-line
      
              // koa-static自己的邏輯還是一樣的,都是調用koa-send
              try {
                  await send(ctx, ctx.path, opts)
              } catch (err) {
                  if (err.status !== 404) {
                      throw err
                  }
              }
          }
      }
      

      koa-static源碼總共就幾十行:https://github.com/koajs/static/blob/master/index.js

      koa-send

      上面我們看到koa-static其實是包裝的koa-send,真正發送文件的操作都是在koa-send里面的。文章最開頭說的幾件事情koa-static一件也沒干,都丟給koa-send了,也就是說他應該把這幾件事都干完:

      1. 通過請求路徑取出正確的文件地址
      2. 通過地址獲取對應的文件
      3. 使用Node.js的API返回對應的文件,并設置相應的header

      由于koa-send代碼也不多,我就直接在代碼中寫注釋了,通過前面的使用,我們已經知道他的使用形式是:

      send (ctx, path, opts)
      

      他接收三個參數:

      1. ctx:就是koa的那個上下文ctx
      2. pathkoa-static傳過來的是ctx.path,看過koa源碼解析的應該知道,這個值其實就是req.path
      3. opts: 一些配置項,defer前面講過了,會影響執行順序,其他還有些緩存控制什么的。

      下面直接來寫一個send方法吧:

      const fs = require('fs')
      const fsPromises = fs.promises;
      const { stat, access } = fsPromises;
      
      const {
          normalize,
          basename,
          extname,
          resolve,
          parse,
          sep
      } = require('path')
      const resolvePath = require('resolve-path')
      
      // 導出send方法
      module.exports = send;
      
      // send方法的實現
      async function send(ctx, path, opts = {}) {
          // 先解析配置項
          const root = opts.root ? normalize(resolve(opts.root)) : '';  // 這里的root就是我們配置的靜態文件目錄,比如public
          const index = opts.index;    // 請求文件夾時,會去讀取這個index文件
          const maxage = opts.maxage || opts.maxAge || 0;     // 就是http緩存控制Cache-Control的那個maxage
          const immutable = opts.immutable || false;   // 也是Cache-Control緩存控制的
          const format = opts.format !== false;   // format默認是true,用來支持/directory這種不帶/的文件夾請求
      
          const trailingSlash = path[path.length - 1] === '/';    // 看看path結尾是不是/
          path = path.substr(parse(path).root.length)             // 去掉path開頭的/
      
          path = decode(path);      // 其實就是decodeURIComponent, decode輔助方法在后面
          if (path === -1) return ctx.throw(400, 'failed to decode');
      
          // 如果請求以/結尾,肯定是一個文件夾,將path改為文件夾下面的默認文件
          if (index && trailingSlash) path += index;
      
          // resolvePath可以將一個根路徑和請求的相對路徑合并成一個絕對路徑
          // 并且防止一些常見的攻擊,比如GET /../file.js
          // GitHub地址:https://github.com/pillarjs/resolve-path
          path = resolvePath(root, path)
      
          // 用fs.stat獲取文件的基本信息,順便檢測下文件存在不
          let stats;
          try {
              stats = await stat(path)
      
              // 如果是文件夾,并且format為true,拼上index文件
              if (stats.isDirectory()) {
                  if (format && index) {
                      path += `/${index}`
                      stats = await stat(path)
                  } else {
                      return
                  }
              }
          } catch (err) {
              // 錯誤處理,如果是文件不存在,返回404,否則返回500
              const notfound = ['ENOENT', 'ENAMETOOLONG', 'ENOTDIR']
              if (notfound.includes(err.code)) {
                	// createError來自http-errors庫,可以快速創建HTTP錯誤對象
                  // github地址:https://github.com/jshttp/http-errors
                  throw createError(404, err)
              }
              err.status = 500
              throw err
          }
      
          // 設置Content-Length的header
          ctx.set('Content-Length', stats.size)
      
          // 設置緩存控制header
          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 = extname(path)
          ctx.body = fs.createReadStream(path)
      
          return path
      }
      
      function decode(path) {
          try {
              return decodeURIComponent(path)
          } catch (err) {
              return -1
          }
      }
      

      上述代碼并沒有太復雜的邏輯,先拼一個完整的地址,然后使用fs.stat獲取文件的基本信息,如果文件不存在,這個API就報錯了,直接返回404。如果文件存在,就用fs.stat拿到的信息設置Content-Length和一些緩存控制的header。

      koa-send的源碼也只有一個文件,百來行代碼:https://github.com/koajs/send/blob/master/index.js

      ctx.type和ctx.body

      上述代碼我們看到最后并沒有直接返回文件,而只是設置了ctx.typectx.body這兩個值就結束了,為啥設置了這兩個值,文件就自動返回了呢?要知道這個原理,我們要結合Koa源碼來看。

      之前講Koa源碼的時候我提到過,他擴展了Node原生的res,并且在里面給type屬性添加了一個set方法:

      set type(type) {
        type = getType(type);
        if (type) {
          this.set('Content-Type', type);
        } else {
          this.remove('Content-Type');
        }
      }
      

      這段代碼的作用是當你給ctx.type設置值的時候,會自動給Content-Type設置值,getType其實是另一個第三方庫cache-content-type,他可以根據你傳入的文件類型,返回匹配的MIME type。我剛看koa-static源碼時,找了半天也沒找到在哪里設置的Content-Type,后面發現是在Koa源碼里面。所以設置了ctx.type其實就是設置了Content-Type

      koa擴展的type屬性看這里:https://github.com/koajs/koa/blob/master/lib/response.js#L308

      之前講Koa源碼的時候我還提到過,當所有中間件都運行完了,最后會運行一個方法respond來返回結果,在那篇文章里面,respond是簡化版的,直接用res.end返回了結果:

      function respond(ctx) {
        const res = ctx.res; // 取出res對象
        const body = ctx.body; // 取出body
      
        return res.end(body); // 用res返回body
      }
      

      直接用res.end返回結果只能對一些簡單的小對象比較合適,比如字符串什么的。對于復雜對象,比如文件,這個就不合適了,因為你如果要用res.write或者res.end返回文件,你需要先把文件整個讀入內存,然后作為參數傳遞,如果文件很大,服務器內存可能就爆了。那要怎么處理呢?回到koa-send源碼里面,我們給ctx.body設置的值其實是一個可讀流:

      ctx.body = fs.createReadStream(path)
      

      這種流怎么返回呢?其實Node.js對于返回流本身就有很好的支持。要返回一個值,需要用到http回調函數里面的res,這個res本身其實也是一個流。大家可以再翻翻Node.js官方文檔,這里的res其實是http.ServerResponse類的一個實例,而http.ServerResponse本身又繼承自Stream類:

      image-20201203154324281

      所以res本身就是一個流Stream,那Stream的API就可以用了ctx.body是使用fs.createReadStream創建的,所以他是一個可讀流,可讀流有一個很方便的API可以直接讓內容流動到可寫流:readable.pipe,使用這個API,Node.js會自動將可讀流里面的內容推送到可寫流,數據流會被自動管理,所以即使可讀流更快,目標可寫流也不會超負荷,而且即使你文件很大,因為不是一次讀入內存,而是流式讀入,所以也不會爆。所以我們在Koarespond里面支持下流式body就行了:

      function respond(ctx) {
        const res = ctx.res; 
        const body = ctx.body; 
        
        // 如果body是個流,直接用pipe將它綁定到res上
        if (body instanceof Stream) return body.pipe(res);
      
        return res.end(body); 
      }
      

      Koa源碼對于流的處理看這里:https://github.com/koajs/koa/blob/master/lib/application.js#L267

      總結

      本文可運行代碼已經上傳GitHub,大家可以拿下來玩玩:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Node.js/KoaStatic

      現在,我們可以用自己寫的koa-static來替換官方的了,運行效果是一樣的。最后我們再來回顧下本文的要點:

      1. 本文是Koa常用靜態服務中間件koa-static的源碼解析。

      2. 由于是一個Koa的中間件,所以koa-static的返回值是一個方法,而且需要符合中間件范式: (ctx, next) => {}

      3. 作為一個靜態服務中間件,koa-static本應該完成以下幾件事情:

        1. 通過請求路徑取出正確的文件地址
        2. 通過地址獲取對應的文件
        3. 使用Node.js的API返回對應的文件,并設置相應的header

        但是這幾件事情他一件也沒干,都扔給koa-send了,所以他官方文檔也說了他只是wrapper for koa-send.

      4. 作為一個wrapper他還支持了一個比較特殊的配置項opt.defer,這個配置項可以控制他在所有Koa中間件里面的執行時機,其實就是調用next的時機。如果你給這個參數傳了true,他就先調用next,讓其他中間件先執行,自己最后執行,反之亦然。有了這個參數,你可以將/test.jpg這種請求先作為普通路由處理,路由沒匹配上再嘗試靜態文件,這在某些場景下很有用。

      5. koa-send才是真正處理靜態文件,他把前面說的三件事全干了,在拼接文件路徑時還使用了resolvePath來防御常見攻擊。

      6. koa-send取文件時使用了fs模塊的API創建了一個可讀流,并將它賦值給ctx.body,同時設置了ctx.type

      7. 通過ctx.typectx.body返回給請求者并不是koa-send的功能,而是Koa本身的功能。由于http模塊提供和的res本身就是一個可寫流,所以我們可以通過可讀流的pipe函數直接將ctx.body綁定到res上,剩下的工作Node.js會自動幫我們完成。

      8. 使用流(Stream)來讀寫文件有以下幾個優點:

        1. 不用一次性將文件讀入內存,暫用內存小。
        2. 如果文件很大,一次性讀完整個文件,可能耗時較長。使用流,可以一點一點讀文件,讀到一點就可以返回給response,有更快的響應時間。
        3. Node.js可以在可讀流和可寫流之間使用管道進行數據傳輸,使用也很方便。

      參考資料:

      koa-static文檔:https://github.com/koajs/static

      koa-static源碼:https://github.com/koajs/static/blob/master/index.js

      koa-send文檔:https://github.com/koajs/send

      koa-send源碼:https://github.com/koajs/send/blob/master/index.js

      文章的最后,感謝你花費寶貴的時間閱讀本文,如果本文給了你一點點幫助或者啟發,請不要吝嗇你的贊和GitHub小星星,你的支持是作者持續創作的動力。

      歡迎關注我的公眾號進擊的大前端第一時間獲取高質量原創~

      “前端進階知識”系列文章:https://juejin.im/post/5e3ffc85518825494e2772fd

      “前端進階知識”系列文章源碼GitHub地址: https://github.com/dennis-jiang/Front-End-Knowledges

      QR1270

      posted @ 2020-12-07 11:50  _蔣鵬飛  閱讀(957)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 国内自拍视频一区二区三区| 精品一区二区久久久久久久网站| 国产免费午夜福利在线播放| 国产中文字幕在线一区| yy111111少妇无码影院| 国产精品嫩草99av在线| 亚洲人精品午夜射精日韩| 中文国产不卡一区二区| 亚洲熟妇少妇任你躁在线观看无码| 又黄又刺激又黄又舒服| 久久久久久伊人高潮影院| 日韩人妻一区中文字幕| 加勒比亚洲天堂午夜中文| 国产一区二区三区导航| 无码帝国www无码专区色综合| 亚洲国产日韩a在线播放| 国产一区二区三区九九视频| 亚洲人成精品久久久久| 国产成人午夜福利院| 国产一区二区三区在线观| 天美传媒mv免费观看完整 | 又大又紧又粉嫩18p少妇| 国产av一区二区三区久久| 吉川爱美一区二区三区视频| 人人爽人人爽人人片a免费| 91精品国产蜜臀在线观看| 国产精品户外野外| caoporn免费视频公开| 国产成人高清精品亚洲一区| 伊在人间香蕉最新视频| 久久热这里这里只有精品| 国产国产午夜福利视频| 精品免费国产一区二区三区四区 | 国产精品久久久久久福利69堂| 国产情侣激情在线对白| 板桥市| 啦啦啦视频在线日韩精品| 亚洲色大成网站WWW永久麻豆| 亚洲精品熟女国产| 自拍偷拍一区二区三区四| 无码人妻丰满熟妇区五十路在线|