手寫一個靜態伺服器

恍然小悟發表於2018-06-18

http-server包

前端開發人員不論是在開發還是測試中都會用到http伺服器,方便快捷的使用更加有助於我們編寫和除錯程式碼,而http-server則是一個十分好用的包,它幾乎不用配置,可以使用任何一個目錄生成一個http伺服器。

簡單說一說http-server的使用。

首先全域性安裝它:

> npm install http-server -g
複製程式碼

安裝成功後就可以使用命令列來啟動一個http服務,進入你想啟動的目錄,使用命令:

> http-server
複製程式碼

開啟瀏覽器訪問http://localhost:8080/即可看到已經使用當前目錄啟動了http server。

然而這裡並不是討論http-server的使用,我們可以仿照http-server包手寫一個功能近似的靜態服務,以更深入的瞭解和學習。

功能說明

一個靜態伺服器都能做什麼?

  • 首先應支援使用計算機上任何一個目錄生成http服務;
  • 可以在瀏覽器中瀏覽目錄結構
  • 可以訪問伺服器上的檔案
  • 支援命令列,可讓使用者自己指定訪問的埠

以上就是http伺服器應該有的大致功能,我們使用node.js手寫程式碼來實現它。

準備工作

自己手寫一個http服務要有基本的檔案讀取,輸入輸出等工作要做,這些大部分功能node.js提供的庫都可以實現,但是還需要有些第三方包的支援,下面簡單說一下。

mime包

mime包是一個可以根據檔案來分析出http內容型別的包,http的內容型別不難理解,當訪問一個html頁面時,瀏覽器會根據嚮應頭的Content-Type型別來呈現內容。

使用npm install mime安裝後即可使用:

const mime = require('mime');
var contentType = mime.getType('/Users/yuet/workspace/static-server/public/node.jpeg');//image/jpeg
複製程式碼

可以看到,傳入正確的檔案路徑即可得到對應的內容型別,這樣可以省去我們自己寫程式碼判斷生成Content-Type

chalk包

chalk包顧名思義,它譯為粉筆,有了它就可以方便的在控制檯輸出內容時使用不同的顏色,同樣安裝它npm install chalk即可使用:

const chalk = require('chalk');
console.log(chalk.red('hello node'));
複製程式碼

以上使用chalk.red(),將輸出內容變為紅色顯示,chalk有很多顏色方法支援,使用它可以突出重點內容,方便使用者檢視。

debug包

debug包可以在node.js環境中幫助我們除錯。

舉例:

var a = require('debug')('worker:a')
  , b = require('debug')('worker:b');
a('doing lots of uninteresting work');
b('doing some work');
複製程式碼

呼叫debug包會返回一個方法,它可以指定作業系統中某環境變數名,以上在作業系統中可以設定兩個環境變數work:awork:b,當前若是為work:a的環境,則只會輸出a方法中的內容,這有利於我們區別開發和生產環境。

預設配置

啟動一個http伺服器,埠、host主機、以及目錄地址這三個內容必不可少,當然使用者在使用時可以更改,但是需要有一個預設的配置:

const path = require('path');
let config = {
  host: '127.0.0.1',
  port: 8080,
  dir: path.join(__dirname, '../public')
};
複製程式碼

指定本機,預設埠號為8080,預設目錄就為當前專案中的/public目錄。將此配置物件匯出供外面使用。

程式碼基本結構

可以封裝一個類,將靜態檔案操作、伺服器的啟停寫在裡面:

const config = require('./config');
class Server {
  constructor() {
    this.config = config;
  }
  //封裝讀取檔案等操作
  handleRequest(req, res) {}
  /**
   * 啟動伺服器
   */
  start() {}
  /**
   * 傳送檔案
   */
  sendFile(req, res, filePath, stat) {}
  /**
   * 傳送錯誤訊息
   */
  sendError(req, res, e) {}
}
new Server().start();
複製程式碼

以上程式碼就是大致的結構,其中將預設的配置物件讀入並掛到類的this上,這樣可以方便的呼叫配置資訊。

核心程式碼

靜態伺服器是基於http的,因此要引入http模組來啟動一個http服務,並監聽埠:

start() {
    let server = http.createServer(this.handleRequest);
    let {host, port} = this.config;
    server.listen({
      host,
      port
    }, () => {
      debug(`伺服器已經啟動在 http://${host}:${chalk.greenBright(port)}/ 上...`);
    });
}
複製程式碼

當啟動了服務,使用者在瀏覽器中輸入了一個地址,此地址可能是一個目錄,也有可能是一個確切的檔案,所以要分別處理。

let {pathname} = url.parse(req.url, true);
let resource = path.join(this.config.dir, pathname);
let s = await stat(resource);
if (s.isDirectory()) {
    //是目錄
} else {
    //是檔案
}
複製程式碼

以上程式碼,首先使用url的模組parse方法將瀏覽器地址解析出來得到絕對路徑,並使用fs模組的stat方法判斷是否是一個目錄,這裡使用了async-await操作簡化了非同步程式碼。

當是目錄的時候,我們的工作就是把目錄下的所有檔案都讀取出來並展示給使用者:

let files = await readdir(resource);//目錄下的所有檔案
files = files.map((i) => {
  return {
    fileName: i,
    path: path.join(pathname, i)
  }
});
let html = ejs.render(temp, {files});
res.setHeader('Content-Type', 'text/html;charset=utf8');
res.end(html);
複製程式碼

其中fs.readdir()方法可以將當前目錄下的所有檔案全部返回,它返回一個Array型別,使用map對映將檔案地址拼接到物件中的目的則是為了ejs模版渲染。這樣在模版中迴圈輸出,即可將當前目錄下的所有檔案展示在瀏覽器中。

ejs模版:

<ul>
  <%files.forEach((i)=>{%>
      <li><a href="<%=i.path%>"><%=i.fileName%></a></li>
  <%})%>
</ul>
複製程式碼

若使用者訪問的是個確切的檔案,那麼使用流的方式輸出即可:

let contentType = mime.getType(filePath)
  ? mime.getType(filePath)
  : 'text/plain';
res.setHeader('Content-Type', contentType + ';charset=utf8');
fs.createReadStream(filePat).pipe(res);
複製程式碼

可以看到使用mime模組得到真正的內容型別,使用流pipe到響應物件中即可。

使用命令列執行

到此為止程式碼已基本實現,但是執行程式時依然使用node命令來執行,這顯然不符合要求,比如http-server包,在全域性安裝之後是使用http-server命令來執行。那麼怎麼也達到這樣的效果呢?

配置命令列工具

在package.json檔案中,使用bin配置節來配置自己的命令:

package.json:

"bin": {
  "my-http-server": "bin/www"
},
複製程式碼

這個配置節的意義是當執行my-http-server命令,執行bin/www檔案。

接下來就是在專案目錄中建立bin/www檔案:

#! /usr/bin/env node
console.log('hello');
複製程式碼

其中井號驚歎號!/usr/bin/env node是要說明去環境變數中找到node所在的目錄,使用node來執行下面的指令碼。

這時,開啟控制檯使用npm link命令把我們的命令連結到全域性

這時就可以正確使用我們自己編寫的命令了:

> my-http-server
複製程式碼

執行結果:

hello
複製程式碼

使用命令列傳遞使用者引數

配置好命令列工具後,下面的任務就是處理命令列引數,以達到使用者自己指定埠的目的。

yargs包

處理命令列引數,完全可以自己寫程式碼擷取,但是第三方yargs已經做好了這部分工作,我們只需安裝使用就好,使用npm安裝:npm install yargs

使用方法:

#! /usr/bin/env node
const yargs=require('yargs').argv;
console.log(yargs);
複製程式碼

此時執行命令列,可以使用引數:

my-http-server --port 3000
複製程式碼

執行結果:

 _: [],
  help: false,
  version: false,
  port: 3000,
  '$0': '/usr/local/bin/my-http-server' }
複製程式碼

可以看到,使用yargs包,可以將命令列引數轉為物件,我們可以很方便的拿到port埠號進行操作。

配置命令並傳參覆蓋預設引數:

#! /usr/bin/env node
const yargs=require('yargs');
const Server=require('../src/app');

let commandParameter=yargs.option('port',{
  alias:'p',
  default:3000,
  type:Number,
  description:'埠號'
}).option('host',{
  default:'127.0.0.1',
  type:String,
  description:'IP地址,預設為127.0.0.1'
}).option('dir',{
  default:process.cwd(),
  type:String,
  description:'靜態檔案地址,預設為當前目錄'
}).usage('my-http-server [options]').argv;

new Server(commandParameter).start();
複製程式碼

這樣,在www檔案中接收到的引數傳入new Server(),覆蓋掉預設引數即可:

constructor(args) {
  this.config = {
    ...config,
    ...args //命令列引數覆蓋預設引數
  };
}
複製程式碼

相關文章