Call Me By Your Name - node.js的小美好

17點發表於2018-06-27

node的出現,真是讓用慣js的前端工程師遇見了愛情,進而大踏步的走向了後端,儘管有時候會被質疑,不被理解。那又有什麼關係。

本文是《一站到底 ---前端基礎之網路》 程式碼的整理。但也是一篇獨立的node零基礎學習筆記。 首先你需要安裝node環境。大家自己去看教程 就好。本文和函數語言程式設計那篇文章是一樣的思路。我們先用先實現。如果有機會我們回過頭再來補理論,其實API也沒啥需要補,有時間我們寫寫node非同步佇列和DC的演算法,但你有什麼不明白的可以隨著檢視文件 。好的,老規矩,我們看看,本文都完成了那些內容。

本文程式碼在github

  • 用node搭建TCP伺服器
  • 用node搭建HTTP伺服器
  • 用node檔案fs模組對檔案讀取,並用流的方式寫入
  • 用url路徑模組,完成了node路由
  • path模組判斷檔案型別
  • 用gzip對檔案進行壓縮
  • 瀏覽器快取協議的實現
  • node處理跨域
  • https的node伺服器的搭建
  • http2的node伺服器的搭建

1 node建立TCP伺服器

const net = require('net');

let server = net.createServer((socket)=>{
   socket.on('data',function (res) {
      console.log(res.toString())
   });
});

server.listen({
   host: 'localhost',
   port: 8080
});
複製程式碼
  • 首先你要知道node用了模組化的思想,你可以require一些模組,
  • net是一個TCP網路 API。我們首先用它來建立一個TCP伺服器
  • 我們引入net模組,通過createServer的方法建立了一個服務
  • 接收到資料的時觸發"data"事件,並將拼接好的報文以引數的形式給我們。
  • 報文是二進位制的buffer資料,我們需要toString方法轉化成字串
  • 然後我們搭建了一個TCP服務,讓監聽localhost,8080埠
  • 我們在terminal中執行 node tcp1.js,這個伺服器就啟動啦
  • 我們現在在瀏覽器裡面訪問localhost:8080
  • 伺服器收到資料後會觸發‘data’事件
  • 我們在terminal中看到了請求頭

這裡我們講一下node的事件機制:

    //events 模組只提供了一個物件: events.EventEmitter
    //EventEmitter 的核心就是事件觸發與事件監聽器功能的封裝。
    var EventEmitter = require('events').EventEmitter; 

    //一個socket物件
    var socket = new EventEmitter();

    //我們在socket物件上繫結data事件,如果是多個函式會被先後呼叫
    socket.on('data', function(res) { 
        console.log(res); 
    }); 

    socket.on('data', function(res) { 
        console.log(res + '111'); 
    }); 

    //我們用emit的方法去觸發事件,在1秒後我們出發,我們觸發事件時,可以傳遞引數。
    setTimeout(function() { 
        socket.emit('data' , "hello" ); 
    }, 1000); 

複製程式碼

我們會在控制檯看到下面的資訊。

Call Me By Your Name - node.js的小美好

這時我們會過頭來看,瀏覽器左下角,是不是一直在顯示等待響應,是因為我們還沒有返回資料啊,那我們給 它返回一些資料。我們知道要符合http格式。

我們將一段符合http格式的資料用socket.write(responseDataTpl)去返回資料

let responseDataTpl = `HTTP/1.1 200 OK
Connection:keep-alive
Date: ${new Date()}
Content-Length: 12
Content-Type: text/plain

Hello world!
`;
複製程式碼
  • 我們觸發 node 01-tcp02.js
  • 在瀏覽器中,我們就能看到返回的Hello world!

問題:我們已經發現了寫出固定格式的http響應報文杯還是比較麻煩的,我們為什麼不能封裝一層呢?

2 node建立HTTP伺服器

2.1 建立HTTP伺服器

const http = require('http');

const server = http.createServer((req, res) => {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('hello world');    //  傳送響應資料 
})

server.on('clientError', (err, socket) => {
    socket.end('HTTP/1.1 400 Bad Request\r\n\r\n')
})

server.listen(10080) 
複製程式碼
  • 我們引入了一個node中http模組,監聽10080埠(預設地址localhost)
  • 我們建立了一個http服務,在請求成功是,返回200狀態碼
  • res.end('hello world')是傳送響應資料的時候帶上'hello world'
  • 我們在terminal中執行 node 02-http-simple.js,這個伺服器就啟動啦
  • 我們現在在瀏覽器裡面訪問localhost:10080
  • 我們看到瀏覽器上顯示'hello world 啊'

問題:那麼這個時候,如果我希望傳進去是一個檔案而不是字串,改怎麼辦呢?

2.2 node檔案模組(fs)

node的檔案模組是非常強大的,可以對檔案進行讀取,增刪改查。這裡我們先講如何讀取的。讀取分兩種一種同步,一種非同步。

const fs = require('fs'); 
const http = require('http');

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/html' });

  // 同步
  // let  data = fs.readFileSync('index.html');
  // res.write(data);    
  // res.end();     //  傳送響應資料 
  
  // 非同步
  fs.readFile('index.html', function (err, data) {
     res.write(data);    
     res.end();     //  傳送響應資料 
  })
})

server.on('clientError', (err, socket) => {
    socket.end('HTTP/1.1 400 Bad Request\r\n\r\n')
})

server.listen(8088) 
複製程式碼
  • 我們引入檔案模組,const fs = require('fs');
  • 同步的時候,我們先讀取,執行後邊的寫入和傳送函式
  • 非同步的時候,我們在非同步讀取的回撥函式中執行寫入和傳送

問題:那麼現在有一個問題,無論是同步還是非同步,我們都需要先讀檔案,再寫入,那麼檔案很大時,對記憶體的壓力就會非常大。我們有沒有什麼辦法,邊讀取邊寫入?

2.3 node流(Stream)

Stream 是一個抽象介面,作用就是能把檔案,讀一點寫一點。這樣不就不用佔很大記憶體了。我們來看看怎麼實現的?

const fs = require('fs');
const http = require('http');

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/html' });
  // let resStream = fs.createReadStream('index.html');
  // resStream.pipe(res);
  //流是可以支援鏈式操作的
  fs.createReadStream('index.html').pipe(res)
})

server.on('clientError', (err, socket) => {
    socket.end('HTTP/1.1 400 Bad Request\r\n\r\n')
})

server.listen(10080)
複製程式碼
  • 用fs.createReadStream('index.html')建立一個可讀流。
  • 用resStream.pipe(res);管道讀寫操作,寫入響應報文
  • 你會發現上面程式碼中我們並沒有用res.end(); 傳送資料 。因為預設情況下,當資料傳送完畢,會自動觸發'end'事件
  • 最後流是支援鏈式操作的,所以你可以一行程式碼就搞定啦

問題:在我們解決了記憶體問題後,你會發現,我們index.html中是有一張圖片沒有載入出來的。原因很簡單。因為無論傳送什麼請求,我們都只返回同樣的操作。那麼我們能如何區分不同的請求呢?

2.4 node路由

我們知道在應用成協議中用URL來表示檔案的位置。區分不同請求的一個重要任務就是區分路徑。那麼對路徑的處理node中提供了一個url模組,讓我們來看看吧。

const fs = require('fs');
const http = require('http');
const url = require("url");

const server = http.createServer((req, res) => {
  //pathname是取到埠號後面的地址
  let pathname = url.parse(req.url).pathname;
  if(pathname === '/') pathname = '/index.html';
  let resPath = '.' + pathname; 

  //判斷路徑是否存在
  if(!fs.existsSync(resPath)){
    res.writeHead(404, {'Content-Type': 'text/html'});
    return res.end('<h1>404 Not Found</h1>');
  }
  //如果存在,將在路徑下的檔案返回給頁面
  res.writeHead(200, { 'Content-Type': 'text/html' });
  fs.createReadStream(resPath).pipe(res)
})

server.on('clientError', (err, socket) => {
    socket.end('HTTP/1.1 400 Bad Request\r\n\r\n')
})

server.listen(10080) 
複製程式碼
  • 我們引入了一個url模組,幫助我們去處理路徑
  • url.parse(req.url)是將一個路徑,幫我們處理成物件,它包含我們常用的路徑屬性
  • 其中有一個屬性是pathname,就是URL埠號和引數之間的路徑,也就是我們訪問的路徑
  • 如果我們直接訪問網站後面不加路徑,我們給預設指向/index.html
  • 相對路徑訪問我們給前面加一個'.'
  • 然後我們用檔案模組提供的existsSync方法去判斷伺服器上是否有這個檔案
  • 如果沒有我們返回404,告訴沒有找到檔案。有就將檔案返回。

問題:那麼現在,我們就能在瀏覽器上看見我們美麗的大娟的圖片了,但是我們在學http的時候知道Content-Type是處理檔案型別的,那麼圖片型別肯定不會是'text/html' ,雖然瀏覽器很智慧幫我顯示出來了,但是我們還是要把這樣的錯誤改過來。

2.5 path模組判斷檔案型別

我們知道,只要改變 'Content-Type'的檔案型別即可。

function getFileType(resPath){
  const EXT_FILE_TYPES = {
    'default': 'text/html',
    '.js': 'text/javascript',
    '.css': 'text/css',
    '.json': 'text/json',

    '.jpeg': 'image/jpeg',
    '.jpg': 'image/jpg',
    '.png': 'image/png',
    //...
  }

  let path = require('path');
  let mime_type = EXT_FILE_TYPES[path.extname(resPath)] || EXT_FILE_TYPES['default'];
  return mime_type;
}
複製程式碼
  • 我們定義了一個getFileType函式,並給出常用的檔案型別和它們Content-Type的值
  • 我們應用了path模組,用path模組上的extname方法取出副檔名
  • 然後跟我們定義的物件去匹配,如果沒有找到,我們就給一個預設的值

Call Me By Your Name - node.js的小美好
你每次修改完node檔案都需要去終端啟動是不是很麻煩。現在再交大家一個熱啟動的小技巧。

sudo npm install supervisor -g

supervisor 02-http-fs-url.js 
複製程式碼
  • 在全域性安裝supervisor
  • 用supervisor代替node去啟動檔案
  • 這樣你在修改node檔案的時候,就不用每次手動去重動終端了

問題:我們大娟的圖片才只有一百多K,如果是圖片很大我們是可以先壓縮再傳輸的

2.5 用gzip對檔案進行壓縮

(1)我們先取出請求頭中的accept-encoding引數,如果引數不存在,我們賦值成''

 let acceptEncoding = req.headers['accept-encoding'];
 if (!acceptEncoding) { acceptEncoding = '';};
複製程式碼

(2)然後我們用正則去判斷acceptEncoding是否用了gzip壓縮,當然這裡可以有多個判斷壓縮格式。這裡我們只寫一個。

if(/\bgzip\b/.test(acceptEncoding)){
      //執行壓縮,並在響應頭中告訴瀏覽器壓縮的格式
  }else{
      //不執行壓縮
  }
複製程式碼

(3)我們需要引用zlib模組對檔案進行壓縮。這裡我們用Gzip,就呼叫Gzip的方法。 然後我們對檔案流先進行一步壓縮,在寫到響應體中。

const zlib = require('zlib');

let raw = fs.createReadStream(resPath);
raw.pipe(zlib.createGzip()).pipe(res);
複製程式碼

(4)最後我們還需要在響應頭中告訴瀏覽器我的檔案已經給你壓縮成什麼格式啦

'Content-Encoding': gzip
複製程式碼

然後我們開兩個終端分別用啟動有gzip和沒有gzip壓縮的

home檔案中放了一張我在頤和園用相機拍的5M的圖片

你可以開啟多個瀏覽器視窗,分別先訪問兩個檔案,可以多測幾遍,你會發現有gzip壓縮的明顯要慢

為什麼會這樣呢,道理很簡單,因為我們的伺服器和瀏覽器都在同一臺電腦上,傳輸速度很快。所以壓縮和解壓的時間就被放大啦。這也告訴我們並不是什麼場景都適合對檔案進行壓縮的。

Call Me By Your Name - node.js的小美好

  • 如果你瀏覽器沒有時間的選項,你可以點選導航欄調出。
  • 在測試的時候,可以把清除快取開啟。

2.6 瀏覽器快取協議的實現

這一節沒有node的新知識,我們對http瀏覽器快取協議進行一個實現。我們也不需要進行壓縮,所以上一節壓縮的內容不會加。

**(1)強快取 ** 強快取我們在響應頭中給一個一週的過期時間 參考程式碼cache.js

Cache-Control : max-age = 604800'
複製程式碼

Call Me By Your Name - node.js的小美好

  • 我們可以看到在第二次重新整理的時候,檔案中的資源就會從瀏覽的快取中取。
  • 如果不想從快取中取,可以強制重新整理,或開啟Disable Cache
  • 強刷的時候,你再看localhost請求頭中會帶上 Cache-Control: no-cache
  • 你會普通重新整理資原始檔會有Cache-Control: no-cache,這是因為資原始檔是從快取中取的,而Cache-Control: no-cache是你上次強刷的時候帶上去的。
  • 如果新開啟一個視窗,再次訪問同一個網頁,不用從快取中取
  • 這就是為什麼,有時候你在開發時,改了js檔案沒有生效,但在另一個視窗開啟看到的是最新檔案的原因

**(2)弱快取 ** 參考程式碼cache2.js

etag需要一個雙引號的字串,然後我們把它寫入響應頭中

 let etagStr = "dajuan";  //etag 要加雙引號

 res.writeHead(200, { 
    'Content-Type': getFileType(resPath),
    'etag' : etagStr
  });
複製程式碼

當再次訪問的時候我們需要判斷一下,if-none-match帶的值於現在etagStr值是否一致。如果一致直接返回304,不用在返回檔案。瀏覽器看到304,就知道了要從快取中拿。

 let etagStr = "dajuan";  //etag 要加雙引號
   if(req.headers['if-none-match'] === etagStr){
    res.writeHead(304, { 
      'Content-Type': getFileType(resPath),
      'etag' : etagStr
    });
   res.end();
 }
複製程式碼

Call Me By Your Name - node.js的小美好
當然,這裡我們只是舉了一個最簡單的例子,真實專案中是不可能把所有的檔案都返回同一個字串的。

2.7 node處理post和get請求

(1)我們首先分別用get 和 post 寫一個表單提交,讓其點選都跳轉到form_result.html,有一行你好,name

  //form.html
  <form action="form_result.html" method="get">
       <p> get: <input type="text" name="name" /></p>
       <input type="submit" value="Submit" />
  </form>
   <form action="form_result.html" method="post">
       <p> post: <input type="text" name="name" /></p>
       <input type="submit" value="Submit" />
  </form>

  //form_result.html
  <div>你好,name</div>
複製程式碼

(2)get方法去處理 參考程式碼method.js

 let pathAll = url.parse(req.url);
 let getArgument = pathAll.query;     //取出引數 name=XXX

 if(pathname === '/form_result.html' && getArgument != undefined){
   let text = fs.readFileSync('form_result.html').toString().replace(/name/, getArgument)
   fs.writeFileSync('form_result.html',text)
 }
複製程式碼
  • 我們知道url.parsl()能讀取url,query就是get方法帶的的引數
  • 當要跳轉的路徑是是'/form_result.html'並且getArgument有值時
  • 我們用檔案模組同步讀取出'form_result.html'的內容
  • 轉換成字串之後,在將表單中的name替換成name=XXX

Call Me By Your Name - node.js的小美好

Call Me By Your Name - node.js的小美好

這時候get提交的表單可以去處理啦,但是post的引數並沒有在URL中,所以對post沒有影響

(3)post方法去處理 參考程式碼method2.js

  req.on('data',(data)=>{
    let text = fs.readFileSync('form_result.html').toString().replace(/name/, 'post'+ data)
    fs.writeFileSync('form_result.html',text) 
  })
複製程式碼
  • post方法是在請求頭中監聽data事件的,請求報文中,有請求體時,被觸發
  • 所以我們在監聽到‘data’事件被觸發時,我們也是執行上面操作
  • 而這個時候如果傳送get請求,就不會被響應
  • 我們學事件知道,我們可以給‘data’繫結多個事件,而每次post請求必然會觸發。這就是對伺服器造成的副作用。

這裡我們留一個問題,我們在處理檔案的時候是同步處理的,如果非同步處理我們改怎麼做?

2.8 node處理跨域

參考程式碼:cors.js cors2.js

  if(req.headers['origin'] ) {
    res.writeHead(200, { 
      'Access-Control-Allow-Origin': 'http://localhost:5000',
      'Content-Type': 'text/html'
    });
    return fs.createReadStream(resPath).pipe(res)
  };  

複製程式碼
  • 我們分別在本地啟動了兩個服務
  • 讓一個埠是5000,另一個埠是9088
  • 我們在5000的埠訪問,cors.html
  • 在html中,我們ajax呼叫9088埠的data.json
  • 這樣就形成了跨域,我們允許5000埠訪問,就會返回資料
  • 如果我們把不填,或者不寫5000埠,你會看到收不到資料

Call Me By Your Name - node.js的小美好

注: 這裡還是有點小問題,第一我只在第一次訪問時,如果埠不符合提示報錯了。我懷疑是不是瀏覽器給伺服器地址加入白名單了。第二為什麼不是書上寫的兩次求情啊。我第一次即使不寫資料,也不會發起第二次請求。不過跨域的效果還是實現了的。

3 https與http2

3.1 https的node伺服器的搭建

知道了原理後,我們在終端生成證照和私鑰吧。

(1)openssl genrsa -out server.key 1024 //生成伺服器私鑰

(2)openssl rsa -in server.key -pubout -out server.pem  // 生成公鑰

  //自己扮演CA機構,給自己伺服器頒發證照,CA機構也需要自己私鑰,CSR檔案(證照籤名請求檔案),和證照

 (3)  openssl genrsa -out ca.key 1024            //生成CA 私鑰
      openssl req -new -key ca.key -out ca.csr   //生成CA CSR檔案
      openssl x509 -req -in ca.csr -signkey ca.key  -out ca.crt  //生成CA 證照

 //生成證照籤名請求檔案
 (4) openssl req -new -key server.key -out server.csr //生成server CSR檔案
  
 //向自己的機構請求生成證照
 (5) openssl x509 -req -CA  ca.crt -CAkey ca.key -CAcreateserial -in server.csr   -out server.crt   //生成server 證照

複製程式碼

注意:資訊隨便填,但提示裡有格式要注意啊,寶寶們。。。

const https = require('https');
const fs = require('fs');

const options = {
  key: fs.readFileSync('./key/server.key'),
  cert: fs.readFileSync('./key/server.crt')
};

https.createServer(options, (req, res) => {
  res.writeHead(200);
  res.end('hello world\n');
}).listen(8000);

複製程式碼
  • 我們引入https模組,填好我們證照和私鑰
  • 剩下的程式碼現在看起來是不是很簡單

伺服器訪問: https://localhost:8000/

Call Me By Your Name - node.js的小美好

  • 這樣我們訪問https就能請求到網頁了
  • 當然會提示我們不安全,繼續就好啦
  • 為啥會提示我們不安全,剛才自己怎麼填的證照,心裡沒數嘛。哈哈哈

3.2 http2的node伺服器的搭建

node的http2是試驗的API。如果node版本比較低,請先升級。我的是v8.11.3

const http2 = require('http2');
const fs = require('fs');

const server = http2.createSecureServer({
  key: fs.readFileSync('./key/server.key'),
  cert: fs.readFileSync('./key/server.crt')
});
server.on('error', (err) => console.error(err));

server.on('stream', (stream, headers) => {
  // stream is a Duplex
  stream.respond({
    'content-type': 'text/html',
    ':status': 200
  });
  stream.end('<h1>Hello World</h1>');
});

server.listen(8443);
複製程式碼
  • 我們還是引入https時建立的私鑰和證照
  • 我們建立http2的服務
  • 在http2中時流的概念。所以我們寫入請求頭。並返回請求體
  • 我們在瀏覽器上訪問:https://localhost:8443/

這樣我們就完成了一個最簡單的http2的訪問啦。

Call Me By Your Name - node.js的小美好

相關文章