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

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

本文是《一站到底 ---前端基礎之網絡》 代碼的整理。但也是一篇獨立的node零基礎學習筆記。 首先你須要安裝node環境。你們本身去看教程 就好。本文和函數式編程那篇文章是同樣的思路。咱們先用先實現。若是有機會咱們回過頭再來補理論,其實API也沒啥須要補,有時間咱們寫寫node異步隊列和DC的算法,但你有什麼不明白的能夠隨着查看文檔 。好的,老規矩,咱們看看,本文都完成了那些內容。css

本文代碼在github html

  • 用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); 

複製代碼

咱們會在控制檯看到下面的信息。java

這時咱們會過頭來看,瀏覽器左下角,是否是一直在顯示等待響應,是由於咱們尚未返回數據啊,那咱們給 它返回一些數據。咱們知道要符合http格式。node

咱們將一段符合http格式的數據用socket.write(responseDataTpl)去返回數據git

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響應報文杯仍是比較麻煩的,咱們爲何不能封裝一層呢?github

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 啊'

問題:那麼這個時候,若是我但願傳進去是一個文件而不是字符串,改怎麼辦呢?ajax

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方法取出擴展名
  • 而後跟咱們定義的對象去匹配,若是沒有找到,咱們就給一個默認的值

你每次修改完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壓縮的明顯要慢

爲何會這樣呢,道理很簡單,由於咱們的服務器和瀏覽器都在同一臺電腦上,傳輸速度很快。因此壓縮和解壓的時間就被放大啦。這也告訴咱們並非什麼場景都適合對文件進行壓縮的。

  • 若是你瀏覽器沒有時間的選項,你能夠點擊導航欄調出。
  • 在測試的時候,能夠把清除緩存打開。

2.6 瀏覽器緩存協議的實現

這一節沒有node的新知識,咱們對http瀏覽器緩存協議進行一個實現。咱們也不須要進行壓縮,因此上一節壓縮的內容不會加。

**(1)強緩存 ** 強緩存咱們在響應頭中給一個一週的過時時間 參考代碼cache.js

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

  • 咱們能夠看到在第二次刷新的時候,文件中的資源就會從瀏覽的緩存中取。
  • 若是不想從緩存中取,能夠強制刷新,或打開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();
 }
複製代碼

固然,這裏咱們只是舉了一個最簡單的例子,真實項目中是不可能把全部的文件都返回同一個字符串的。

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

這時候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端口,你會看到收不到數據

注: 這裏仍是有點小問題,第一我只在第一次訪問時,若是端口不符合提示報錯了。我懷疑是否是瀏覽器給服務器地址加入白名單了。第二爲何不是書上寫的兩次求情啊。我第一次即便不寫數據,也不會發起第二次請求。不過跨域的效果仍是實現了的。

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/

  • 這樣咱們訪問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的訪問啦。

相關文章
相關標籤/搜索