# nodeJS ## 特點 > - 優點 > - 與 js 與法完全相同, 前端好上手 > - 效能還行(chrome V8 引擎) > - 缺點 > - 庫不多 > - 用處 > - 小型服務器, 中間層 > - 中間層用處: 增加安全性, 提升性能(存放緩存等雜事), 前後端交互方便 > - 工具: 測試, 構建(grunt, gulp, webPack...), 抓取 > - nodeJS 還是單線程, 單進程, > 不過 nodeJS 有使用非阻塞異步交互 ( 非堵塞IO ) 來增加性能 > - 不搞多進程線程的原因在於 nodeJS 強項並非性能, 想要性能請用 java C, > nodeJS 強項在於簡單, 多進程線程太複雜了, 要用找別人( 真有個性== ) > - java 擅長處理網絡, 安全性 > - nodeJS 擅長處理中間層, 緩存, API > - python 擅長人工智能 > - c 的性能完勝 ## 運行 > - `$ node 檔名` ## 簡單導覽模塊 ### http > - 服務器有很多種, 例如 apache, nginx, iis, node,... > - 瀏覽器也很多種, 例如 chrome, IE, ff, opera,... > - 兩者都必須遵循 http 規範, 所以如果我要用 nodeJS 搭建服務器, 我也要遵守http > - 但如果要所有人都完全了解 http, 那太麻煩了, 所以 node 有提供相應的API > <img src='https://i.imgur.com/X2Q2yBx.png' style='width:200px;'/> > - http 模塊 > - HTTP > - HTTP/2 > - HTTPS #### 最基本的串接服務器 ```javascript= const http = require('http'); // 引入模塊 let server = http.createServer((request, response)=> { // 創建服務器 console.log('有人來了') response.write('haha'); // 輸出 response.end(); // 結束輸出 }); server.listen(5566); // 監聽 /* shell $ node 1.js 有人來了 有人來了 ^C */ ``` > - url 輸入 `http://localhost:5566/` > - 畫面輸出 haha > - Q. 為什麼輸出了兩次有人來了? ```javascript= const http = require('http'); let server = http.createServer((request, response)=> { console.log(request.url); // 看看請求了啥~ response.write('haha'); response.end(); }); server.listen(5566); /* shell $ node 1.js / # 我 url 輸入的請求 /favicon.ico # favicon.ico 請求~ 一般新的瀏覽器都會自己求求看小圖標, 舊 IE 就不會 ^C */ ``` > #### `require(module)` > - 引入模塊, 就是 python 的 `from xxx` > #### `createServer(cb(request, response){})` > - http 模塊裡的方法, 顧名思義, 創服務器, 返回一個服務器對象 > - 參數是一個 callback, 有人訪問就會調用 > - 該 callback 有兩個形參 `request` `response`, 必須站在服務器的角度來看 > - `request` : 請求, (輸入) > - `response` : 發送, (輸出) > - `response.write(content)` > - 發送的內容 > - `response.end()` > - 告訴瀏覽器我送完了, 否則瀏覽器會一直傻等 > - 這個 callback 再往內寫一層調用邏輯: > http 是應用層協議, 基於 tcp => socket > ```javascript > socket.on('data', ()=>{ > req = 解析請求(head, body, 加密,...); > res = 另一個 socket > > cb(req, res); > }) > ``` > #### `server.listen([port[, host[, backlog]]][, callback])` > - 監聽 > - 一個服務器可能同時有多個程式在運行, 客戶端訪問服務器時, > 需要告訴服務器端口號, 否則誰知道你找誰, 而 `listen(port)` > 就是設置這個程序的端口號, 等客戶端來找你 > - 設置端口號要避開重複的, 否則大家電話號碼都設110, 誰知道怎麼報警 > - 默認端口是跟著協議的, > 不過當然你爽怎麼設就怎麼設, 不一定要跟默認走, 只是別人會找不到你而已 > - http: 80 > - ftp: 21 > - mysql: 3306 > - ... > - host 指定網卡 > #### 問題: > - 訪問 5566 端口, 不論指定哪個文件, 都是返回haha #### 版本二 > - 拿到指定文件, 並返回相應內容 ```javascript= let http = require(`http`); let server = http.createServer((req, res)=> { console.log(req.url); // 訪問不同, 就給相應的內容 switch (req.url){ case `/a`: res.write(`aa`); break case `/b`: res.write(`bb`); break default: res.write(`cc`); } res.end(); }); server.listen(5566); ``` > #### 問題 > - 不可能全部文件都寫在一個 js 上 > - 如果有一萬個文件, 我switch 要判斷一萬次? > 而且一萬的文件的內容全部寫在 每個相應 res.write() 裡面, 這js文件大小可想而知 > - 甚至是圖片檔 (二進制) 怎麼直接 write()? > - 延伸: JS 很難自己處理二進制, 他都直接交給底層接口處理 > #### 解決 > - 解藕 > - 服務器就是純粹做轉發的工作 > - 檔案存在硬碟中 > - 檔案最好放在一個對外的資料夾, 接收到請求時, 一律往該資料夾找, > 因為不可能整個電腦的資料都公開給大家找, > 例如 xamp 就放在 /www 裡, xampp 就放在 /htdocs 裡 #### 版本三 ```shell $ tree . ├── 1.js └── www # 創一個資料夾, 接到訪問就來這找 └── 1.html ``` ```javascript= const http = require(`http`); const fs = require(`fs`); let server = http.createServer((req, res)=> { // 一收到訪問, 就往 www 找 // 注意 req.url 本來最前面就會帶一個 / 了, 所以別在加 // fs.readFile(`www/1.html`, cb) fs.readFile(`www${req.url}`, (err, data)=> { if (err) { console.log(0); res.writeHeader(404); // res.write(`Not Found`); } else { console.log(1); res.write(data); } res.end(); }); console.log(2); // res.end(); //=> 千萬別寫這, 一定掛 }); server.listen(5566); /* shell 2 1 重要: - 先 2 再 1 - 因為 readFile 是異步, 所以別把 end() 放在同步寫, 否則一定報錯 - Error [ERR_STREAM_WRITE_AFTER_END]: write after end */ ``` > #### writeHeader() > - 改報文頭的 > - 修改前 > <img src='https://i.imgur.com/stmqfoi.png' style='width:300px;'> > - 修改後 > <img src='https://i.imgur.com/tmFKTxM.png' style='width:300px;'> ### https > - https 需要申請憑證(證書), > 免費證書: 在中國可以考慮阿里雲的, 在中國以外可以考慮[letsencrypt](https://letsencrypt.org/) #### 客戶端 ```javascript= $.ajax({ url: 'https://localhost:8080/' }) ``` #### 服務器 ```javascript= let https = require(`https`); let fs = require(`fs`); // https 需要這個證書 let options = { key: fs.readFileSync(`agent2-key.pem`), cert: fs.readFileSync(`agent2-cert.pem`) } https.createServer(options, (res, req)=>{}).listion(8080); ``` ### Assertion Testing `assert(Boolean 表達式, msg)` > - 如果表達式不為 true, 報錯, 輸出 msg > - 我 google 翻譯, 他叫斷言 ```javascript= const assert = require('assert'); console.log((function (x,y) { assert(arguments.length == 2, '請傳兩個參數'); // 如果表達式執行結果為false, 輸出後面那句話 return x+y // })()) // AssertionError: 請傳兩個參數 })(11,22)) // 33 ``` ### Buffer & FileSystem > #### 預備知識 > - 服務器幾乎99.9都是異步, 否則他服務你一個人就啥都不用幹了 > - 檔案操作非常慢, 所以也都是異步, 否則開個檔案電腦就卡住了 > - 雖然 nodeJS 也有提供同步版(sync), 只是很少用 #### Buffer > - 幫助 nodeJS 處理各種二進制的一種數據結構 > - 不過為了方便, 顯示時是以十六進制的方式顯示 #### fs.readFile() `fs.readFile(path[, options], cb(err, data){})` > - `path`: 檔案 > - `cb` > - `err` : 錯誤訊息 > - `data` : 正確數據 > - 用 Buffer 數據處理 > - 對於服務器來說, 都必須用同一套標準來處理文件, > 如果用文本處理而文件是圖檔 (二進制文件) 呢? > 所以通通都用 Buffer(二進制) 來處理 ```shell $ tree . ├── 1.js └── 1.txt ``` ```txt 123 ``` ```javascript= let fs = require('fs'); fs.readFile('1.txt', (err, data)=>{ // console.log(err); // 正確時: // null // 錯誤時: /* [Error: ENOENT: no such file or directory, open '2.txt'] { errno: -2, code: 'ENOENT', syscall: 'open', path: '2.txt' } */ // console.log(data); // 正確時: <Buffer 31 32 33 0a> //=> Buffer!!! (註) // 錯誤時: undefined if (err) { console.log(err); } else { console.log(data.toString()); // 123 //=> 必須確認不是二進制數據才轉 } }) ``` ```javascript= // 31 32 33 由來 // 字符編碼 '123'.charCodeAt(0); // 49 '123'.charCodeAt(1); // 50 '123'.charCodeAt(2); // 51 // 49 59 51 的十六進制就是 31 32 33 // 所以打印顯示 Buffer 是顯示十六進制 ``` #### fs.writeFile() `fs.writeFile(file, data[, options], cb(err){})` > - 寫入檔案 > - 如有該檔案, 會覆蓋檔案 ```shell $ tree . └── 1.js ``` ```javascript= let fs = require('fs'); fs.writeFile('1.txt', '123', err=> { if (err) { console.log(err); } else { console.log('success'); } }) ``` ```shell $ node 1.js success $ tree . ├── 1.js └── 1.txt $ cat 1.txt 123 ``` #### fs.appendFile() `fs.appendFile(path, data[, options], callback)` > - 基本上跟 `writeFile()` 用法一樣 > - 差別在於他是接著從後面繼續寫, `writeFile()` 是直接重寫, 覆蓋原檔 ### Crypto > - 簽名, 不是加密 > - 提供各種 hash 算法, 例如 md5, sha1,.. > - 單向散列的: 只能把值加密, 不能把加密的解密 > - Q. 那網路上那些解密怎麼做到的? > - 暴力破解的, 把加密結果都記起來, 解密時輸入加密後的值時, 並不是解密, > 而是做一個查詢的動作, 畢竟加密結果都是一樣的, 查一下就知道了 > - Q. 最安全的加密是? > - 如果有辦法做到每次加密都不同, 密鑰長度>內容長度, 那就很安全 > 問題是這流通困難, 所以不存在 > - 目前相對安全的加密方法為: RSA > > - Q. 前端加密方法? > - HTTPS > - 安裝插件(一般都是銀行之類的比較需要這麼麻煩, 其他HTTPS就行了) #### crypto.createHash() `crypto.createHash(method)` > - 創建一個加密的對象, 參數傳加密的方法 > #### crypto.createHash().update() > - `crypto.createHash(method).update(str)` > - 新增要加密的字串 > #### crypto.createHash().digest() > - `crypto.createHash(method).digest(顯示方法)` > - 拿到加密後的值 > - 參數為顯示方法, 可選的, 沒傳默認 `<Buffer>` 顯示 ```javascript= const crypto = require('crypto'); // let obj = crypto.createHash(`md5`); let obj = crypto.createHash(`sha1`); // 可以一次寫 // obj.update('123456'); // 也可以慢慢更新 obj.update('123'); obj.update('4'); obj.update('56'); console.log(obj.digest()); // <Buffer e1 0a dc 39 49 ba 59 ab be 56 e0 57 f2 0f 88 3e> // Buffer 數據類型 console.log(obj.digest(`hex`)); // e10adc3949ba59abbe56e057f20f883e //=> 轉 16 進制顯示 ``` #### 雙層加密 > - 既然他都把值記下來了, 那雙層加密就是把加密後的值再加密一次 > - 如果數據庫夠大, 可能重複加密很多次還是會被破 > - 可以打亂原本的重複加密規則, 例如加密後拼接一組字串(私鑰),再拿去加密 > 如果沒人知道你的這組字串, 那就很難破解 > 這也是為什麼忘記密碼都要創新的, 因為存在數據庫的是雙層加密又混私鑰的字串, 還原你的密碼太麻煩了, 叫你弄新的比較快 ```javascript= const crypto = require('crypto'); function md5(str) { let obj = crypto.createHash(`md5`); obj.update(str); return obj.digest(`hex`); } console.log(md5(md5(`123456`))); // 14e1b600b1fd579f47433b88e8d85291 //=> 基本上即使是雙層, 還是有可能被紀錄而查到 //=> 解決辦法就是多一層自己知道的字串, 如果這串沒有被人知道, 那就安全 console.log(md5(md5(`123456`)+`apple5566nodie`)); // 97cb9c4848c8b5b7d4ddb921c6e9f400 //=> 這個就很難解了 ``` > <img src='https://i.imgur.com/JMOzsbi.png' style='width: 300px'/> <img src='https://i.imgur.com/IdC4Rpe.png' style='width: 300px'/> ### OS ```javascript= let os = require('os'); console.log(os.cpus()); ``` ```shell $ node 1.js [{...},{...},{...},{...}] $ # 我的電腦四核的, 所以有四組 ``` ### Path ```javascript= let path = require('path'); let p = '/aaa/bbb/ccc/d.jpg' console.log(path.dirname(p)); // /aaa/bbb/ccc console.log(path.basename(p)); // d.jpg console.log(path.extname(p)); // .jpg ``` ```javascript= const path = require('path'); // 組路徑 // 這兩個組起來都一樣 console.log(path.join(__dirname, './a/c')); // /Users/.../01_webpack/a/c console.log(path.resolve('./a/c')) // /Users/.../01_webpack/a/c ``` ### Events > - 後端沒有前端那些事件, 例如點擊啦之類的 > - 但後端有後端的對列, 例如事件到達觸發 > - 最初為了解決異步問題, 但現在異步漸漸被 async/await 取代 > - 與一般函數最大區別在於Events 可以解藕, > 因為Events 是以事件名作為依據而非函數, 而寫完就函數就綁死在那了, 無法輕易變動, 不過這在大項目才比較有感覺, 菜雞如我無法感受 #### EventEmitter > - 事件發射器 > - 用 new 來實例 > - Events 模塊最常用的就是這個而已 #### on `emit(事件名, cb)` > - 監聽, 接收 > - 事件名隨便取 #### emit `emit(事件名, 調用cb的實參)` > - 發送, 觸發 ```javascript= const Event = require('events').EventEmitter; let eve = new Event(); // 監聽 eve.on('apple', (x,y)=> { console.log(x,y); }) eve.on('apple', (a, b)=>{ console.log(a+b); }) // 觸發 eve.emit('apple', 1,2); /* * 1 2 * 3 */ // 與一般function 的差別? // Event 可以解藕, 這個 apple 變量已經綁著這個 function 了 // 上面那個Event實例的監聽可以隨便綁各種函數,想怎麼綁就怎麼綁 function apple(x,y) { console.log(x,y); } apple(1,2); // 1 2 ``` ### Query String > - 查詢字串 > - url 後面那個參數就是queryString > https://tw.search.yahoo.com/search?fr=yfp-search-sb&p=js 的 fr=yfp-search-sb&p=js ```javascript= let qs = require(`querystring`); let obj = qs.parse(`fr=yfp-search-sb&p=js`); console.log(obj); // [Object: null prototype] { fr: 'yfp-search-sb', p: 'js' } ``` ### url > - 就整個 url #### url.parse() `url.parse(url, Boolean)` > - `Boolean` : > - 如果 true, 表示 query 也要解析, false 就表示不用 > - 預設為 false ```javascript= let u = require(`url`); let obj = u.parse(`https://tw.search.yahoo.com/search?fr=yfp-search-sb&p=js`, true); console.log(obj); /* parse 二參為 false Url { protocol: 'https:', slashes: true, auth: null, host: 'tw.search.yahoo.com', port: null, hostname: 'tw.search.yahoo.com', hash: null, search: '?fr=yfp-search-sb&p=js', query: 'fr=yfp-search-sb&p=js', pathname: '/search', path: '/search?fr=yfp-search-sb&p=js', href: 'https://tw.search.yahoo.com/search?fr=yfp-search-sb&p=js' } */ /* parse 二參為 true Url { protocol: 'https:', slashes: true, auth: null, host: 'tw.search.yahoo.com', port: null, hostname: 'tw.search.yahoo.com', hash: null, search: '?fr=yfp-search-sb&p=js', query: [Object: null prototype] { fr: 'yfp-search-sb', p: 'js' }, pathname: '/search', path: '/search?fr=yfp-search-sb&p=js', href: 'https://tw.search.yahoo.com/search?fr=yfp-search-sb&p=js' } */ ``` ### DNS <img src='https://i.imgur.com/VOGjcCn.jpg' style='width: 300px;'> ```shell $ ping google.com PING google.com (172.217.24.14): 56 data bytes 64 bytes from 172.217.24.14: icmp_seq=0 ttl=51 time=28.735 ms ... $ # google 的 172.217.24.14 => 域名 ``` #### dns.resolve() `dns.resolve(url, cb)` > - 解析指定網址的 dns > - 網路操作慢, 所以經常使用cb > - cb 兩參數, 錯誤訊息(err)與域名(res) ```javascript= let d = require(`dns`); // d.resolve(`google.com`, (err, res)=>{ d.resolve(`5566nodiehaha.com`, (err, res)=>{ if (err) { console.log(err); } else { console.log(res); } }) // [ '172.217.24.14' ] // Error: queryA ENOTFOUND 5566nodiehaha.com // 有些網站有好幾個域名, 例如百度 // [ '39.156.69.79', '220.181.38.148' ] ``` ### 其他常見模塊 > - C > - C++ Addons > - C/C++ Addons with N-API > - 用 c 寫一些插件給 nodeJS 使用 > - 移植可能會有問題, 因為各個平台的 c 語言編譯器可能不同 > - 多進程 > - Child Processes > - Cluster > - Process > - 網路 > - tcp => nodeJS 使用 Net 模塊來使用 tcp > - udp => nodeJS 使用 UDP/Datagram 模塊 來使用 udp > - 域名 > - DNS > - Domain > - 流操作 stream > - 連續數據都是流: 影音流, 網路流, 文件流, 語音流 > - TLS/[SSL](https://zh.wikipedia.org/wiki/%E5%82%B3%E8%BC%B8%E5%B1%A4%E5%AE%89%E5%85%A8%E6%80%A7%E5%8D%94%E5%AE%9A) > - 加密, 安全 > - https 就是 http 基於 SSL > - 現在常用的加密協議都是依據 SSL 實現的 > - ZLIB: gz壓縮 ## 數據交互 > - Web 服務器主要工作 > - 返回文件 > - 數據交互(GET POST) > - GET > - 數據在 head 裡 (url裡) > - <=32k > - POST > - 數據在 body 裡 > - <= 1G > - 數據庫 ### 表單 > #### GET > - 寫一個表單, 開啟服務器, 表單 action 記得前面要傳對端口 ```shell $ tree . ├── 1.js └── www └── 1.html ``` ```htmlmixed= <head> <meta charset='utf-8'> </head> <body> <form action='http://localhost:5566/apple' method='GET'> <input name='a'> <input name='b'> <input type='submit'> </form> </body> ``` ```javascript= let http = require(`http`); let url = require(`url`); let server = http.createServer((req, res)=>{ // console.log(req.url); // /apple?a=5566&b=nodie //=> GET 數據就在 url 上 // console.log(url.parse(req.url)); /* Url { ..., query: 'a=5566&b=nodie', pathname: '/apple', ... } */ let {query, pathname} = url.parse(req.url, true); console.log(query); console.log(pathname); res.end(); }) server.listen(5566); // 輸入表單後 chorme 刷新的網址 // http://localhost:5566/apple?a=5566&b=nodie /* shell [Object: null prototype] { a: '5566', b: 'nodie' } /apple */ ``` > #### POST > - 大部分的大數據都是切成小包小包的 > - 有些服務器一次只跟一個客戶端連線, > 如果客戶端需要傳100G的檔案, 那這個服務器就都不用服務別人了 > - 容錯率提高, 如果傳錯了, 就只要補傳那個錯的小包即可 > - 也就是說後端收到數據的時候不會一次收到, 而是小包小包收 > #### Post 筆記 > - `http.createServer(cb(req,res) {})` > - `req.on('data', cb(data){})` > - 每次收到數據時觸發 > - 收到的數據可以不用特別驗證, 因為 http 是基於 tcp > - `req.on('end', cb(){})` > - 收完所有數據後觸發 ```htmlmixed= <body> <form action='http://localhost:5566/apple' method='POST'> <input name='a'> <input name='b'> <input type='submit'> </form> </body> ``` ```javascript= let http = require(`http`); let querystring = require(`querystring`) let server = http.createServer((req, res)=>{ let str = ''; // NodeJS 的事件跟瀏覽器的事件當然不一樣, 後端不用點擊交互等 // on 就跟 addEventListener 一樣是註冊監聽事件用的 // data 事件是每次收到數據都會觸發 // 不用特別驗證這個data, 畢竟http是基於 tcp, 掉包機會比較小 req.on(`data`, data=> { str+=data; }) // 數據全部收完後觸發 req.on(`end`, ()=>{ console.log(str); let d = querystring.parse(str); console.log(d); }) res.end(); }) // 輸入表單後 chrome 的 url => http://localhost:5566/apple // POST 數據本來就不在 url 裡 /* shell a=5566&b=nodie [Object: null prototype] { a: '5566', b: 'nodie' } */ ``` ### 接口 <img src='https://i.imgur.com/bqhxQu5.jpg' style='width: 400px;'> > - 數據庫是後端最重要的資產, 不可能讓前端直接看到 > 所以必須要有通過一個第三方來通信, 這個街口可以用各式語言來完成 > - 接口與前端通信必須討論一套規則, 前後端要傳什麼格式給對方等問題, 例如 > ```txt > // 註冊 > 前端 => 後端 /reg?user=xxx&pass=ooo > 後端 => 前端 {"error": 1, "msg": "xxxx"} 或 > {"error": 0, "msg": "success"} > > // 登入 > 前端 => 後端 /login?user=xxx&pass=ooo > 後端 => 前端 {"error": 1, "msg": "xxxx"} 或 > {"error": 0, "msg": "success"} > ``` > - 安全性: > - 一切來自前端的都必須校驗 > - 前後端都必須對數據校驗 > - 前端為了用戶體驗, 避免格式不符就送, 浪費網路 > - 後端為了安全性 ```shell $ tree . ├── 1.js └── www ├── 1.html └── 2.html ``` `$ vi 1.js` ```javascript= // 服務器 let http = require(`http`); let querystring = require(`querystring`) let url = require(`url`); let fs = require(`fs`); let users = {} // 測試用而臨時存放數據的地方 let server = http.createServer((req, res)=>{ // 獲取URL數據 let {query, pathname} = url.parse(req.url, true); // 獲取 body 數據 let str = ''; req.on(`data`, data=> { str+=data; }) req.on(`end`, ()=>{ // 解析 body 數據 let post = querystring.parse(str); // console.log(query, pathname, post); // 根據上面假設的討論, 註冊與登入時, 傳過來的參數會有 user 跟 pass let {user, pass} = query; // 判斷客戶端想要幹嘛 switch (pathname) { // 想註冊, 驗證數據有沒有問題 // 必須假設前端送來的資料都有問題 case '/reg': if (!user) { // 根據上面假設討論的回傳數據 res.write(`{"error": 1, "msg": "Give Me UserName, Please"}`) } else if (!pass) { res.write(`{"error": 1, "msg": "Give Me Password, Please"}`) } else if (!/^\w{6,10}$/.test(user)){ res.write(`{"error": 1, "msg": "UserName Not as required"}`) } else if (/['"]/.test(pass)){ res.write(`{"error": 1, "msg": "PassName Not as required"}`) } else if (users[user]) { res.write(`{"error": 1, "msg": "UserName has been registered"}`) } else { users[user] = pass; res.write(`{"error": 0, "msg": "Success"}`) } res.end(); break; // 想登入, 驗證數據 case '/login': if (!user) { res.write(`{"error": 1, "msg": "Give Me UserName, Please"}`) } else if (!pass) { res.write(`{"error": 1, "msg": "Give Me Password, Please"}`) } else if (!/^\w{6,10}$/.test(user)){ res.write(`{"error": 1, "msg": "UserName Not as required"}`) } else if (/['"]/.test(pass)){ res.write(`{"error": 1, "msg": "PassName Not as required"}`) } else if (!users[user]) { res.write(`{"error": 1, "msg": "UserName has not been registered"}`) } else if (users[user] != pass) { res.write(`{"error": 1, "msg": "UserName or Password is incorrect"}`) } else { res.write(`{"error": 0, "msg": "Success"}`) } res.end(); break; // 其他, 表示要拿資料 default: // 送到對外的資料夾找 fs.readFile(`www${pathname}`, (err, data)=>{ if (err) { res.writeHeader(404); res.write(`not Found`); } else { res.write(data); } res.end(); }) } }) }) server.listen(5566); ``` ```htmlmixed= <!-- 表單測試 --> <form action='http://localhost:5566/reg' method='GET'> <!-- <form action='http://localhost:5566/login' method='GET'> --> <input name='user'> <input name='pass'> <input type='submit'> </form> ``` ```htmlmixed= <!-- AJAX 測試 --> <head> <meta charset='utf-8'> <script> window.onload = function () { let btn1 = document.querySelector(`#btn1`); let btn2 = document.querySelector(`#btn2`); btn1.onclick = function () { let u = document.querySelector(`#user`).value; let p = document.querySelector(`#pass`).value; // 1. 創建 XHR 對象 let xhr = new XMLHttpRequest(); // 2. 寫資料 xhr.open(`GET`, `/reg?user=${u}&pass=${p}`, true); // 3. 傳送 xhr.send(); xhr.onreadystatechange = function () { // 傳完之後 if (xhr.readyState === 4) { // console.log(1) // 4. 判斷是否『 通信 』成功 if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) { // console.log(xhr.responseText); // 5. 判斷 註冊/登入 成功與否 // 5.1 處理回傳的字串 let data = JSON.parse(xhr.responseText); // 5.2 依據假設回傳的數據 // 會有 error 的對象, 1 代表數據有問題, // 1 Boolean 為 ture, 進去表示數據有問題 if (data.error) { console.log(`Error: `+ data.msg); } else { console.log(`Success: ` + data.msg); } } else { console.log(`error`); } } } } // 登入一模一樣, 差別在 url 變成 login 而已 btn2.onclick = function () { let u = document.querySelector(`#user`).value; let p = document.querySelector(`#pass`).value; let xhr = new XMLHttpRequest(); xhr.open(`GET`, `/login?user=${u}&pass=${p}`, true); xhr.send(); xhr.onreadystatechange = function () { if (xhr.readyState === 4) { if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) { console.log(xhr.responseText); let data = JSON.parse(xhr.responseText); if (data.error) { console.log(`Error: `+ data.msg); } else { console.log(`Success: ` + data.msg); } } else { console.log(`error`); } } } } } </script> </head> <body> <input id='user' name='user'><br/> <input id='pass' name='pass'><br/> <input type='button' value='註冊' id='btn1'> <input type='button' value='登入' id='btn2'> </body> ``` ### 接口風格 > - 定義接口可以隨便定, 不過很多人會依照某些風格來制定 > - 叫流行的例如 [RESTful](https://zh.wikipedia.org/wiki/表现层状态转换) ## Post 的 multipart 上傳 > - 表單的三種 POST > - application/x-www-form-urlencoded : 一堆 &&& > - multipart/form-data; boundary=----WebKitFormBoundaryWBQBnwHG6EAMjPQA > - `enctype='multipart/form-data'` > - text/plain : 沒啥用 #### multipart/form-data > - 上面用 str 拼接的問題 > 1. 如果遇到不是用 urlencoded 會有問題 > 2. 如果遇到傳二進制檔案, 更有問題 > - 因為multipart/form-data傳進來的數據都是二進制, 直接轉string圖檔直接亂碼 ```shell $ tree . ├── 1.js ├── 1.txt └── post.html ``` ```txt 123456789 ``` ```htmlmixed= <!-- <form action='http://localhost:5566/upload' method='POST'> --> <form action='http://localhost:5566/upload' method='POST' enctype='multipart/form-data'> <input name='a' /> <input name='b' /> <input type='file' name='fl'/> <input type='submit'/> </form> ``` ```javascript= let http = require(`http`); let s = new http.createServer((req, res)=> { console.log(req.url); let str = ''; // 用字符串拼接, req.on(`data`, data=>{ // console.log(`data: ${data}`); str += data; }) req.on(`end`, ()=> { console.log(str); res.end() }) }); s.listen(5566); ``` ```shell $ # 1. 用 urlencoded 傳簡單的文檔 /upload a=aaa&b=bbb&fl=1.txt # => 1. 沒資料, 只有名字, 有名字沒啥用啊 $ # 2. 用 urlencoded 傳圖檔 /upload a=aaa&b=bbb&fl=photo.jpeg # => 2. 沒資料, 只有名字, 有名字沒啥用啊 $ # 3. 用 multipart/form-data 傳簡單的文檔 /upload ------WebKitFormBoundaryeQhHBmWUW3ZBgVrZ Content-Disposition: form-data; name="a" aaa ------WebKitFormBoundaryeQhHBmWUW3ZBgVrZ Content-Disposition: form-data; name="b" bbb ------WebKitFormBoundaryeQhHBmWUW3ZBgVrZ Content-Disposition: form-data; name="fl"; filename="1.txt" Content-Type: text/plain 123456789 # => 3. 有資料, 可是多很多HTTP 規定的東西 ------WebKitFormBoundaryeQhHBmWUW3ZBgVrZ-- $ # 4. 用 multipart/form-data 傳圖檔 /upload ------WebKitFormBoundary9UAJnYPs6PKANNWJ Content-Disposition: form-data; name="a" aaa ------WebKitFormBoundary9UAJnYPs6PKANNWJ Content-Disposition: form-data; name="b" bbb ------WebKitFormBoundary9UAJnYPs6PKANNWJ Content-Disposition: form-data; name="fl"; filename="photo.jpeg" Content-Type: image/jpeg ����JFIF��� ( %!1!%)+...383-7(-.+ ....一堆亂碼, 略 # 4. 直接發生慘案 ------WebKitFormBoundary9UAJnYPs6PKANNWJ-- ``` ### Buffer > - nodeJS 用來專門存放二進制數據的地方, 類似陣列的地方 > - https://www.runoob.com/nodejs/nodejs-buffer.html > - ps. [官方](https://nodejs.org/zh-cn/docs/guides/buffer-constructor-deprecation/)說不要用 `new Buffer()`, 要用 `Buffer.alloc()` 或 `Buffer.form()` 來代替 > - `new Buffer(number)` => `Buffer.alloc(number)` > - `new Buffer(string[, encoding])` => `Buffer.from(string[, encoding])` > - `new Buffer(...arguments)` => `Buffer.from(...arguments)` #### Buffer.concat([]) `Class Method: Buffer.concat(list[, totalLength])` > - 合併 Buffer 數據 > - 參數是一個 list ```javascript= let a = new Buffer(`a`); // Buffer.from(`a`); let b = new Buffer(`b`); let c = Buffer.concat([a,b]); console.log(a,b,c); // <Buffer 61> <Buffer 62> <Buffer 61 62> ``` > - 解析 multipart/form-data 數據 ```txt # 原本數據 ------WebKitFormBoundaryeQhHBmWUW3ZBgVrZ # 分割符 Content-Disposition: form-data; name="a" # 數據描述 aaa # 數據 ------WebKitFormBoundaryeQhHBmWUW3ZBgVrZ # 分割符 Content-Disposition: form-data; name="b" # 數據描述 bbb # 數據值 ------WebKitFormBoundaryeQhHBmWUW3ZBgVrZ # 分割符 Content-Disposition: form-data; name="fl"; filename="1.txt" # 數據描述 Content-Type: text/plain # 數據描述 123456789 # 文件內容 ------WebKitFormBoundaryeQhHBmWUW3ZBgVrZ-- # 分割符 # 改造1. --------------------------------------------- <分割符> <數據描述> <數據> <分割符> <數據描述> <數據> <分割符> <數據描述> <數據描述> <文件內容> <分割符>-- # 改造2. --------------------------------------------- # 事實上每行尾都有 /r/n 來換行 <分割符>\r\n <數據描述>\r\n \r\n <數據>\r\n <分割符>\r\n <數據描述>\r\n \r\n <數據>\r\n <分割符>\r\n <數據描述>\r\n <數據描述>\r\n \r\n <文件內容>\r\n <分割符>--\r\n # 改造3. --------------------------------------------- <分割符>\r\n<數據描述>\r\n\r\n<數據>\r\n <分割符>\r\n<數據描述>\r\n\r\n<數據>\r\n <分割符>\r\n<數據描述>\r\n<數據描述>\r\n\r\n<文件內容>\r\n <分割符>--\r\n ``` ``` # 根據 Buffer 的合併數據 Buffer.concat 的方法, 想用 Buffer 處理, 使用 Array 可能會很方便 # 1. 用 <分割符> 來切割 [ 空, \r\n<數據描述>\r\n\r\n<數據>\r\n, \r\n<數據描述>\r\n\r\n<數據>\r\n, \r\n<數據描述>\r\n<數據描述>\r\n\r\n<文件內容>\r\n, --\r\n ] # 特點: # - 文件數據有兩項描述, 與普通數據有別 # - 數據內容的開始在『 第一個 』 \r\n 後面 # - 但不是所有 \r\n 數據後面都是數據的開始 # - 如果我文件空兩行, 數據就兩個 \r\n 了 1 2 #=> 1\r\n\r\n2 # - 頭尾兩項看起來沒啥用到 # - 每項頭尾都有 \r\n 包著 # 2. 清掉沒用到的東西 [ <數據描述>\r\n\r\n<數據>, <數據描述>\r\n\r\n<數據>, <數據描述>\r\n<數據描述>\r\n\r\n<文件內容>, ] # 3. 用 \r\n\r\n 把數據切開, 會得到兩種陣列 - 普通數據 [<數據描述>, <數據>] - 文件數據 [<數據描述>\r\n<數據描述>, <文件內容>] ``` #### `Buffer.prototype.indexOf()` `buf.indexOf(value[, byteOffset][, encoding])` > - 查找 > - `return` : > - `value in buf` : 找到的位置 > - `-1` : 找不到 > - `value` : `<str>` | `<Buffer>` | `<int>` | `<Uint8Array>` > - `byteOffset` : `<int>`, 起始索引 ```javascript= let a = Buffer.from(`aa-o-bb-o-cc`); // let b = a.indexOf(Buffer.from(`-o-`)); let b = a.indexOf(`-o-`); // 直接給字串也行, 因為他會自己轉 Buffer console.log(b); // 2 ``` #### `Buffer.prototype.slice(start, end-1)` > - 擷取 ```javascript= let a = Buffer.from(`aa-o-bb-o-cc`); let c = a.slice(3,5); // 擷取 [3] 跟 [4] console.log(a); // <Buffer 61 61 2d 6f 2d 62 62 2d 6f 2d 63 63> console.log(c); // <Buffer 6f 2d> console.log(c.toString()); // o- ``` #### `Buffer.prototype.split() ?` ```javascript= let a = Buffer.from(`aa-o-bb-o-cc`); let c = a.splic(`-o-`); // a.splic is not a function ``` > - 還沒有這個方法, 官方文檔都沒找到, 只能自己寫 ```javascript= let a = Buffer.from(`aa-o-bb-o-cc`); Buffer.prototype.split = Buffer.prototype.split || function (x) { let a = []; let n = 0; let cur = 0; // 如果有找到 n 就存找到的位置 while ((n = this.indexOf(x, cur)) !== -1) { a.push(this.slice(cur, n)) // 把找到前的東西切出來丟進陣列 cur = n + x.length; // 指針要改成分割元素的後面那個 } a.push(this.slice(cur)); // 最後有一段會切不到, 指針切到底就是最後一段 return a }; let c = a.split(`-o-`); console.log(c); // [ <Buffer 61 61>, <Buffer 62 62>, <Buffer 63 63> ] console.log(c.map(buffer=>buffer.toString())); // [ 'aa', 'bb', 'cc' ] ``` ### 處理 multipart/form-data 數據 #### `req.headers` > - 獲取傳進來的 headers ```javascript= const http = require(`http`); let s = new http.createServer((req, res)=> { console.log(req.url); // 看客戶端 url 傳啥 console.log(req.headers); // 看客戶端表頭傳啥 let arr = []; req.on(`data`, data=>{ console.log(data.toString()); // 看客戶端數據(body)傳啥 arr.push(data); }) req.on(`end`, ()=> { let data = Buffer.concat(arr) res.end() }) }); s.listen(5566); ``` ```shell $ # 沒有傳資料時 #=> 沒有 Content-type /upload { host: 'localhost:5566', connection: 'keep-alive', 'upgrade-insecure-requests': '1', 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36', 'sec-fetch-user': '?1', accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'sec-fetch-site': 'none', 'sec-fetch-mode': 'navigate', 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'en-US,en;q=0.9,zh-TW;q=0.8,zh;q=0.7' } $ # 傳 urlencoded # 'content-type': 'application/x-www-form-urlencoded' /upload { host: 'localhost:5566', connection: 'keep-alive', 'content-length': '20', 'cache-control': 'max-age=0', 'upgrade-insecure-requests': '1', origin: 'null', 'content-type': 'application/x-www-form-urlencoded', #=> HERE 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36', 'sec-fetch-user': '?1', accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'sec-fetch-site': 'cross-site', 'sec-fetch-mode': 'navigate', 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'en-US,en;q=0.9,zh-TW;q=0.8,zh;q=0.7' } a=aaa&b=bbb&fl=1.txt $ # 傳 multipart/form-data # 'content-type': 'multipart/form-data; boundary=----WebKitFormBoundaryEfBUaWBDtW6ESf6K' # 他會傳 ---WebKitFormBoundaryEfBUaWBDtW6ESf6K' 分隔福進來 /upload { host: 'localhost:5566', connection: 'keep-alive', 'content-length': '369', 'cache-control': 'max-age=0', 'upgrade-insecure-requests': '1', origin: 'null', 'content-type': 'multipart/form-data; boundary=----WebKitFormBoundaryEfBUaWBDtW6ESf6K', #=> HERE 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36', 'sec-fetch-user': '?1', accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'sec-fetch-site': 'cross-site', 'sec-fetch-mode': 'navigate', 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'en-US,en;q=0.9,zh-TW;q=0.8,zh;q=0.7' } ------WebKitFormBoundaryEfBUaWBDtW6ESf6K Content-Disposition: form-data; name="a" aaa ------WebKitFormBoundaryEfBUaWBDtW6ESf6K Content-Disposition: form-data; name="b" bbb ------WebKitFormBoundaryEfBUaWBDtW6ESf6K Content-Disposition: form-data; name="fl"; filename="1.txt" Content-Type: text/plain 123456789 ------WebKitFormBoundaryEfBUaWBDtW6ESf6K-- ``` ### 整理思緒 > #### 觀察 multipart 的 content-type ``` // Header 'content-type': 'multipart/form-data; boundary=----WebKitFormBoundaryEfBUaWBDtW6ESf6K', // Content ------WebKitFormBoundaryEfBUaWBDtW6ESf6K ... ------WebKitFormBoundaryEfBUaWBDtW6ESf6K-- ``` > - multipart 傳來的 header 會有 `content-type` 跟使用的分隔符 > - `EfBUaWBDtW6ESf6K` 是隨機的, > 避免文檔內容寫到 ------WebKitFormBoundary 而造成混淆 > - Header 的 boundary 只有四個 ---- > - 內容的前面兩個是跟最後兩個 \-\- 對應的 > #### 觀察兩種數據陣列 ``` // 普通數據 Content-Disposition: form-data; name="a" // 文件數據 Content-Disposition: form-data; name="fl"; filename="1.txt"\r\n Content-Type: text/plain ``` > - 描述用 `; ` 分隔 > - 普通數據的描述只有一行 > - 文件數據的描述有多行(\r\n) > #### 處理數據 > - 邏輯就是假設全部都是二進制, 把數據全部丟到 Buffer 裡, > 然後利用上面那些規則挑出真正的二進制, 並且利用那些規則來拿到相應的數據 > - 去 headers 拿邊界 > - 用邊界切數據 > - 拿掉前後項 > - 拿到前後 \r\n > - 用 \r\n\r\n 切割數據, 拿到數據描述與數據 > - 利用普通數據的數據描述只有一行 ( 沒有\r\n ) 來判斷哪些是普通數據 > 普通數據的數據描述可以直接跟數據內容組起來 > - 二進制數據的數據描述也可以切一切取出來 > - 二進制數據的數據內容直接丟給 `fs` 來處理 ```shell $ tree . ├── 1.js # 服務器 ├── 1.txt # 想要上傳的文件 ├── common.js # 放剛剛寫的 split ├── post.html # 表單上傳文件 └── upload # 要把上傳的文件存放的地方 └── 1.txt # 執行後成功存放的文件 ``` ```javascript= Buffer.prototype.split = Buffer.prototype.split || function (x) { let a = []; let n = 0; let cur = 0; while ((n = this.indexOf(x, cur)) !== -1) { a.push(this.slice(cur, n)) cur = n + x.length; } a.push(this.slice(cur)); return a }; ``` ```javascript= const http = require(`http`); const common = require(`./common.js`) const fs = require(`fs`) let s = new http.createServer((req, res)=> { // 1. 把數據全部存到 arr 裡, 不要輕易轉 str, 避免悲劇 let arr = []; req.on(`data`, data=>{ arr.push(data); }) req.on(`end`, ()=> { // 1.1 假設所有數據都是二進制, 把數據全部丟進 Buffer 裡 let data = Buffer.concat(arr) // 之後整理完數據要存放用的 let post = {}; let files = {}; // 2. 拿到邊界 if (req.headers[`content-type`]) { let ct = req.headers[`content-type`]; // console.log(ct); // multipart/form-data; boundary=----WebKitFormBoundaryamBq0Ond7btQNd5P // headers 數據前面只有四個 -, 補兩個進來 let Boundry = `--` + ct.split(`; `)[1].split(`=`)[1]; // console.log(Boundry); // ------WebKitFormBoundaryamBq0Ond7btQNd5P // 3. 用邊界分割數據 // console.log(data); // <Buffer 2d 2d 2d 2d 2d 2d ... 319 more bytes> let arr = data.split(Boundry) // 記得在原型寫一個 split // console.log(arr); /* [ <Buffer >, <Buffer 0d 0a 43 6f 6e 74 ... 1 more byte>, <Buffer 0d 0a 43 6f 6e 74 ... 1 more byte>, <Buffer 0d 0a 43 6f 6e 74 ... 53 more bytes>, <Buffer 2d 2d 0d 0a> ] */ // 4. 拿掉前後項 arr.shift(); arr.pop(); // 5. 拿掉前後 \r\n arr = arr.map(buffer=>buffer.slice(2,buffer.length-2)); // console.log(arr.map(b=>b.toString())); /* [ 'Content-Disposition: form-data; name="a"\r\n\r\naaa', 'Content-Disposition: form-data; name="b"\r\n\r\nbbb', 'Content-Disposition: form-data; name="fl"; filename="1.txt"\r\n' + 'Content-Type: text/plain\r\n' + '\r\n' + '123456789\n' ] */ // 6. 用 \r\n\r\n 切割出數據描述與數據 arr.forEach(b=>{ let n = b.indexOf(`\r\n\r\n`); let discription = b.slice(0, n); let content = b.slice(n+4); // console.log(discription.toString(), `||` ,content.toString()) /* Content-Disposition: form-data; name="a" || aaa Content-Disposition: form-data; name="b" || bbb Content-Disposition: form-data; name="fl"; filename="1.txt" Content-Type: text/plain || 123456789 */ // 7. 數據分類 // - 利用普通數據的描述只有一行的特性, 可以區分哪些不是二進制 // - 二進制文件的描述也可以直接轉, 只有內容需要保持 Buffer // 7.1 描述直接轉字串 discription = discription.toString(); // 7.2 判斷是普通數據還是二進制數據 if (discription.indexOf(`\r\n`)==-1) { // 普通數據隨便轉 content = content.toString(); // console.log(discription); // Content-Disposition: form-data; name="a" // 8. 數據描述處理 // - 數據描述是 `; ` 來區隔 // 8.1 拿到每個 name let name = discription.split(`; `)[1].split(`=`)[1]; // console.log(name) //=> "a" // 8.2 把雙引號拿掉 name = name.substring(1,name.length-1); // console.log(name); // 9. 組合數據 post[name] = content; } else { // console.log(discription); /* Content-Disposition: form-data; name="fl"; filename="1.txt" Content-Type: text/plain */ // 8.3 處理非普通數據描述 // - 非普通數據的描述有多行, 利用 \r\n 可以切出每行 let [line1, line2] = discription.split(`\r\n`); // - 處理每個數據的相應 let [, name, filename] = line1.split(`; `); name = name.split(`=`)[1]; name = name.substring(1, name.length-1); filename = filename.split(`=`)[1]; filename = filename.substring(1, filename.length-1); type = line2.split(`: `)[1]; // console.log(name, filename, type); // fl 1.txt text/plain // console.log(content); // <Buffer 31 32 33 34 35 36 37 38 39 0a> // 10. 把二進制數據直接用 fs 處理 fs.writeFile(`upload/` + filename, content, err=>{}) } }) console.log(post); } res.end() }) }); s.listen(5566); ``` ### uuid /guid > - https://www.npmjs.com/package/uuid > - 可以產生一串亂代碼 #### 安裝 > - `$ npm init` > - `$ npm install uuid -D` > - `-D == --save-dev` > - 寫入 devDependencies > - 開發環境 > - vs `-S == --save` > - 寫入 dependencies > - 生產環境 > > - 更新 npm `npm install -g npm` , MAC 可能會遇到權限問題, 前面加個 sudo 就搞定了 ```shell $ tree . ├── 1.js ├── 1.txt ├── common.js ├── node_modules │ └── uuid # 安裝的 uuid │ ├── AUTHORS │ ├── CHANGELOG.md │ ├── LICENSE.md │ ├── README.md │ ├── bin │ │ └── uuid │ ├── index.js │ ├── lib │ │ ├── bytesToUuid.js │ │ ├── md5-browser.js │ │ ├── md5.js │ │ ├── rng-browser.js │ │ ├── rng.js │ │ ├── sha1-browser.js │ │ ├── sha1.js │ │ └── v35.js │ ├── package.json │ ├── v1.js │ ├── v3.js │ ├── v4.js │ └── v5.js ├── package-lock.json ├── package.json # npm init ├── post.html └── upload └── 1.txt $ cat package.json { "name": "02", "version": "1.0.0", "description": "", "main": "1.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "devDependencies": { "uuid": "^3.3.3" # uuid 環境 } } ``` #### 使用 ```javascript= const uuid = require(`uuid/v4`); console.log(uuid()); console.log(uuid().replace(/\-/g, '')); // 清 - 可以省空間 ``` ```shell $ node test.js 713bf640-cc45-4739-8a8f-ca7e0e13e823 03722a972409407aa5737c89366e2b05 $ node test.js 08de6563-977c-4ce1-9e58-f4dba31d0164 5498330f4ef34d4aaffecf5f30f5b673 $ # 每次執行都會產生一組亂碼 ``` ### 使用 uuid 當檔名 > - 隨機產生一串字串 > - uuid 是有協議的 https://tools.ietf.org/html/rfc4122 ```javascript= const uuid = require(`uuid/v4`); // 引入 uuid, v4 是第四版, 他還有 135版 ... ... if (discription.indexOf(`\r\n`)==-1) { let name = discription.split(`; `)[1].split(`=`)[1]; name = name.substring(1,name.length-1); content = content.toString(); post[name] = content; } else { let [line1, line2] = discription.split(`\r\n`); let [, name, filename] = line1.split(`; `); name = name.split(`=`)[1]; name = name.substring(1, name.length-1); filename = filename.split(`=`)[1]; filename = filename.substring(1, filename.length-1); type = line2.split(`: `)[1]; console.log(name, filename, type); // 使用 uuid 當檔名 let path = `upload/${uuid().replace(/\-/g, '')}` fs.writeFile(path, content, err=>{ if (err) { console.log('') } else { // 把數據存起來 files[name] = {path, filename,type}; // 註 console.log(files); console.log(`成功`); } }) } ``` ```javascript= // 註 let a = 1, b = 2 let o = {} o[`haha`] = {a,b} o // {haha: {…}} // haha: {a: 1, b: 2} // __proto__: Object ``` ```shell $ node 1.js fl photo.jpeg image/jpeg { fl: { path: 'upload/ec661cc44feb4f5cbdc72f280493635f', filename: 'photo.jpeg', type: 'image/jpeg' } } 成功 $ tree ... └── upload └── ec661cc44feb4f5cbdc72f280493635f ``` > - 副檔名改一下就可以用了 > - 副檔名是給人看的, 機器間交流是不需要副檔名的 > <img src='https://i.imgur.com/9qN4e1W.png' style='width: 300px;'/><img src='https://i.imgur.com/NVCH6zc.png' style='width: 300px;'/> > - 服務器如何判斷文件類型 > - 實用性很低, 基本上存起來就好了 > - 判斷方法 > - 簡易判斷: 副檔名 > - 嚴謹判斷: 分析文件結構, 每個檔案的結構都拿出來做 if 判斷, 非常麻煩 > #### 問題 > - 等到所有數據都傳完才處理 => 在 end() 的 cb 處理 > - 應該要收到一些解析一些 > - fs.readFile & fs.writeFile 的問題 ```javascript const http = require(`http`); const fs = require(`fs`); let s = http.createServer((req, res)=>{ // readFile 會把所有數據都存到內存後才回調 // - 佔用內存 // - 內存都快炸了, CPU 還是空的 // - 資源利用不充分 // - 一下磁盤忙得要死, 網絡閒得要死 // - 一下網絡忙得要死, 磁盤閒得要死 fs.readFile(`www${req.url}`, (err, data)=>{ res.write(data); }) }); s.listen(5566); ``` > - 解決: 流 (讀一點數據, 發ㄧ點數據) ## 流 > - 連續的數據都是流 > - 所有的流都有 `on` > - 操作方法: > - 讀取流: `rs` `req` > - 寫入流: `ws` `res` > - 讀寫流: 壓縮 加密 > - 利用 `pipe` 來接通流 > - 想像流是兩個地點, pipe 是水管, > - 水往低處流, 簡單說的話就是會自己調配 > - 生產者與消費者模型 > <img src='https://i.imgur.com/CCVL0Nf.jpg' style='width: 250px'> > - 簡單的說, 當能夠寫時才會繼續讀, > - 因為大部分都是讀比寫快 > - 讀寫各自有一塊緩沖區(數組), 當寫的緩衝區滿了時, 會觸發事件, 告訴讀別再送了 > 此時讀的地方就會暫時先不讀 > - 等到寫的陣列清空後, 會再告訴讀可以繼續了 > - 不過當然也有可能寫比讀快, 那就不會觸發爆滿事件而一直讀一直寫 ### 讀取流 > - `fs.createReadStream(<file>)` > - 返回讀到的數據 > - `http.createSever((req,res)=>{}) ` 的 `request` 其實也是讀取流 > - 因為 req 也是流, 所以他有 `on` > #### `fs.createReadStream.on('error', err=>{})` > - 讀取錯誤觸發 cb ### 寫入流 > - `fs.createWriteStream` > - `http.createServer((req,res)=>{})` 的 `response` 其實也是寫入流 > - 因為 res 也是流, 所以他有 `on` > #### `fs.createWriteStream.on('finish', ()=>{})` > - 寫入完成後觸發 > - 寫入完成的名字不是 `end` ! #### 文件流 ```shell $ tree . ├── 1.png ├── test.js └── upload ``` ```javascript= let fs = require(`fs`); let rs = fs.createReadStream(`1.png`); // let rs = fs.createReadStream(`3.png`); //=> 讀取失敗測試 let ws = fs.createWriteStream(`2.png`); rs.pipe(ws); // 流有方向性, 先讀後寫 rs.on(`data`, data=>{ console.log(`讀取資料`) }) rs.on(`end`, ()=>{ // rs.on(`end`) 讀取完還會有一些還沒寫完 console.log(`讀取完成`) }) // 錯誤處理 rs.on(`error`, err=>{ console.log(`讀取失敗`) }) ws.on(`finish`, ()=>{ // ws 結束的名字不是 end!!! console.log(`寫入完成`) }) ``` ```shell $ node test.js 讀取資料 or 讀取失敗 讀取完成 寫入完成 $ tree . ├── 1.png ├── 2.png ├── test.js └── upload ``` #### 網路流 ```javascript= let fs = require(`fs`); let http = require(`http`); let s = http.createServer((req, res)=>{ let rs = fs.createReadStream(`www${req.url}`); // 讀取資料 rs.pipe(res); // 把資料流出去 rs.on(`error`, err=>{ // 除錯 res.writeHead(404); res.write(`Not Found`); res.end(); }) }) s.listen(5566); ``` ### 讀寫流 > - 壓縮, 加密, ... > - ps. 音樂影片圖片類檔案的壓縮率不高, 所以有些人傾向不壓縮這些東西 ```shell $ tree -sh --size . ├── [280k] JQuery.js └── [ 259] gzip.js ``` ```javascript= const fs = require(`fs`); const zlib = require(`zlib`); let rs = fs.createReadStream(`JQuery.js`); let ws = fs.createWriteStream(`JQuery.js.gz`); let gz = zlib.createGzip(); // 創建一個 gz 壓縮對象 rs.pipe(gz).pipe(ws); // 讀寫流: 一邊在讀一邊在寫 ws.on(`finish`, ()=>{ console.log(`壓縮完畢`) }) ``` ```shell $ node gzip.js 壓縮完畢 $ tree -sh --size . ├── [280k] JQuery.js ├── [ 83k] JQuery.js.gz #=> 壓縮成功 └── [ 259] gzip.js $ # 解壓正常~ $ tree -sh --size . ├── [280k] JQuery\ 2.js #=> 正常解壓 ├── [280k] JQuery.js ├── [ 83k] JQuery.js.gz └── [ 259] gzip.js ``` > - gz 是主流壓縮方法之一 #### 網路傳輸壓縮 ```shell $ tree . ├── server.js └── www ├── JQuery.js └── temp.html ``` ```javascript= const http = require(`http`); const fs = require(`fs`); const zlib = require(`zlib`); http.createServer((req, res)=>{ let rs = fs.createReadStream(`www${req.url}`); let gz = zlib.createGzip(); // rs.pipe(res); //=> 比較直接流與壓縮後流的差別 res.setHeader(`Content-Encoding`, `gzip`); //=> 必須讓瀏覽器知道傳過去的是壓縮檔 rs.pipe(gz).pipe(res); rs.on(`error`, err=>{ console.log(err); res.setHeader(`Content-Encoding`, ``); // NOT FOUND 不是壓縮檔, 所以必須把頭改回來, // 否則瀏覽器用 gzip 去解析這兩個字根本看不懂而無法顯示 res.writeHeader(404); res.write(`NOT Found`); res.end(); }) }).listen(5566); ``` ```htmlmixed= <script src='JQuery.js'></script> 123 ``` > - 壓縮前後 > ![](https://i.imgur.com/ZJUiA1a.png) > ![](https://i.imgur.com/2KIdybm.png) > - html 非常微小的變大, 我猜是表頭多丟了幾個字( `setHeader()` ) > - 如果沒有在表頭告訴瀏覽器編碼方法, 瀏覽器根本不認識這檔案, > 然後就會跳出下載, 叫使用者自己打開來看 > <img src='https://i.imgur.com/Sc9UQPZ.png' style='width: 200px'> > - 參考 Yahoo 的 Response Header > - `content-encoding: gzip` > - 設置後, 主流的瀏覽器就會自己解壓了 > ![](https://i.imgur.com/EMyMcKm.png) > #### `request.setHeader(name, value)` > - 設定 header > - `setHeader` 是設定 header 對象裡的屬性, 這只是設置 > vs. `writeHeader`: write 寫完就發出去了 > - 如果寫了 writeHeader 後再寫 setHeader, 程序會掛掉 ```javascript= const http = require(`http`); http.createServer((req, res)=>{ res.writeHeader(999); res.setHeader(`content-type`, `text/html`); res.write(5566); res.end(); }).listen(5566); ``` ```shell $ node server.js Error: Cannot set headers after they are sent to the client ``` ## 緩存 > - 瀏覽器的緩存一般都非常大, Chrome 之所以這麼快, 就是因為他用空間換時間 > - 有沒有可能本地的緩存修改時間比瀏覽器的新? > - 正常情況下不會, 但如果使用者自己去改過那個檔案, 那就有可能 > - 強制不使用緩存的絕招? > - 改變 url > - 服務器配置 > - 事實上標準的瀏覽器每次都會訪問服務器一次, 雖然很多瀏覽器都不是標準的 ### 觀察 XAMPP 傳輸 ```txt // x 為重複 key, v 為多出來的 key // 第一次請求 // - REQUEST x Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 x Accept-Encoding: gzip, deflate x Accept-Language: en-US,en;q=0.9,zh-TW;q=0.8,zh;q=0.7 x Connection: keep-alive x Host: 192.168.64.2 x Referer: http://192.168.64.2/test/ x Upgrade-Insecure-Requests: 1 x User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36 // - RESPONSE Status Code: 200 OK x Accept-Ranges: bytes v Connection: Keep-Alive x Content-Length: 4 x Content-Type: text/plain x Date: Thu, 09 Jan 2020 01:59:06 GMT x ETag: "4-59bab53846ab9" v Keep-Alive: timeout=5, max=100 x Last-Modified: Thu, 09 Jan 2020 01:57:06 GMT #=> 主要重點 x Server: Apache/2.4.41 (Unix) OpenSSL/1.1.1d PHP/7.4.1 mod_perl/2.0.8-dev Perl/v5.16.3 // 第二次請求 // - REQUEST x Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 x Accept-Encoding: gzip, deflate x Accept-Language: en-US,en;q=0.9,zh-TW;q=0.8,zh;q=0.7 v Cache-Control: max-age=0 x Connection: keep-alive x Host: 192.168.64.2 v If-Modified-Since: Thu, 09 Jan 2020 01:57:06 GMT #=> 主要重點 v If-None-Match: "4-59bab53846ab9" x Referer: http://192.168.64.2/test/ x Upgrade-Insecure-Requests: 1 x User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36 // - RESPONSE Status Code: 304 Not Modified #=> 主要重點 x Accept-Ranges: bytes x Content-Length: 4 x Content-Type: text/plain x Date: Thu, 09 Jan 2020 01:59:47 GMT x ETag: "4-59bab53846ab9" x Last-Modified: Thu, 09 Jan 2020 01:57:06 GMT #=> 主要重點 x Server: Apache/2.4.41 (Unix) OpenSSL/1.1.1d PHP/7.4.1 mod_perl/2.0.8-dev Perl/v5.16.3 ``` > - 第一次請求: S > C => `Last-Modified: Thu, 09 Jan 2020 01:57:06 GMT` > - 第一次請求時, 服務器會發資料日期給客戶端 > - 第二次請求: > - C > S => `If-Modified-Since: Thu, 09 Jan 2020 01:57:06 GMT` > - S > C => `Status Code: 304 Not Modified` > - 第二次請求時, 客戶端會告訴服務器自己手上的版本 > - 服務器查證一下, 版本相同304, 版本不同 202 > - 所以緩存最重要的就是文件的最新修改日期 ### fs.stat() `fs.stat(path[, options], cb(err, stat)=>{})` > - 查看檔案資訊 ```javascript= const fs = require(`fs`); fs.stat(`./www/temp.html`, (err, stat)=>{ if (err) { console.log(err); } else { console.log(stat); console.log(stat.mtime); // 2020-01-08T16:13:14.858Z console.log(typeof stat.mtime); // object console.log(stat.mtime.constructor); // [Function: Date] // 返回的是一個 Date 的實例 } }) ``` ```shell $ tree . ├── test.js └── www └── temp.html $ node test.js Stats { dev: 16777224, # 設備號 mode: 33188, # 文件模式 nlink: 1, # 連接數, linux 跟 os 可以 ssh 連接, 所以可能不是2 uid: 501, gid: 20, rdev: 0, blksize: 4096, ino: 830972, size: 38, blocks: 8, atimeMs: 1578500585213.3013, mtimeMs: 1578499994858.087, ctimeMs: 1578500058928.0815, birthtimeMs: 1578499994853.8013, atime: 2020-01-08T16:23:05.213Z, mtime: 2020-01-08T16:13:14.858Z, # 修改時間 ctime: 2020-01-08T16:14:18.928Z, # 建檔時間 birthtime: 2020-01-08T16:13:14.854Z # 檔案生日==? 應該跟 ctime 同意思 } ... ``` ### Date.prototype.toUTCString() > - 把日期對象轉 string > - `Date.prototype.toGMTString()` 是一樣的東西, MDN 說別用這個 ```javascript= let oDate = new Date(); console.log(oDate.toUTCString()); // Thu, 09 Jan 2020 02:53:40 GMT console.log(oDate.toGMTString()); // Thu, 09 Jan 2020 02:53:40 GMT ``` ### 緩存實現過程 > - 利用 `fs.stat` 拿到檔案最新修改時間 > - 利用 `req.headers` 查看瀏覽器是否有 `if-modified-since` > - 沒有: 表示瀏覽器沒有存過這個檔案在緩存中 => 發一份新的 202 > - 有: 表示瀏覽器以前有存過, => 比較本機與瀏覽器的版本新舊 > - 本機比較新: 發一份新的 202 > - 本機沒有比較新: 304 叫瀏覽器讀取自己內存就好了 > - 兩者差距 > ![](https://i.imgur.com/dO7cTcN.png) > ![](https://i.imgur.com/y0VNbR3.png) ```javascript= const fs = require(`fs`); const http = require(`http`); const url = require(`url`); http.createServer((req, res)=>{ let pathname = url.parse(req.url)[`pathname`]; // console.log(url.parse(req.url)); // 1. 拿到文件的修改日期 fs.stat(`www${pathname}`, (err, stat)=>{ if (err) { // 錯誤處理 // console.log(err); res.writeHeader(404); res.write(`Not Found`); res.end(); } else { // 2. 判斷瀏覽器有沒有拿過這個檔案 if (req.headers[`if-modified-since`]) { // 2.2 有拿過就拿他的修改時間來跟服務器的修改時間對比 // console.log(req.headers[`if-modified-since`]); let cliDate = new Date(req.headers[`if-modified-since`]); cliDate = Math.floor(cliDate.getTime() / 1000); // 盡量避免小數 console.log(cliDate); let serDate = Math.floor(stat.mtime.getTime() / 1000); console.log(serDate); // 2.2.1 比對誰新 if (serDate > cliDate) { // 2.2.2 服務器比較新就發一份 sendFile(); console.log(1); } else { // 2.2.3 服務器沒有比較新就 304 console.log(2); res.writeHeader(304); res.write(`Not Modified`); res.end(); } } else { // 2.1 沒有就是第一次訪問這個文件, 發一次 console.log(0); sendFile(); } function sendFile() { let rs = fs.createReadStream(`www${pathname}`); // 每次傳檔案都要傳檔案最新修改時間 res.setHeader(`last-modified`, stat.mtime.toUTCString()); rs.pipe(res); // stat 那邊錯誤處理只表示他那時讀得到 // 不代表檔案都沒錯, 也可能在這期間又發生啥事, 有處理有保佑 rs.on(`error`, err=>{ res.writeHeader(404); res.wtite(`Not Found`); res.end(); }) } } }) }).listen(5566); ``` ```shell $ node server.js 0 #=> 第一次請求, 1578543584 #=> 相同版本請求, 304 1578543584 2 1578543584 #=> 修改檔案後請求, 重傳 1578543819 1 1578543819 #=> 修改檔案再次請求, 304 1578543819 2 ``` ```txt // - 第一次請求文件 // REQUEST ... // RESPONSE Status Code: 200 OK ... last-modified: Thu, 09 Jan 2020 04:19:44 GMT ... // - 第二次請求文件 // REQUEST ... If-Modified-Since: Thu, 09 Jan 2020 04:19:44 GMT ... // RESPONSE Status Code: 304 Not Modified last-modified: Thu, 09 Jan 2020 04:19:44 GMT // - 修改文件後請求 // REQUEST ... // If-Modified-Since: Thu, 09 Jan 2020 04:19:44 GMT ... // RESPONSE Status Code: 200 OK ... // last-modified: Thu, 09 Jan 2020 04:23:39 GMT ... ``` ### 緩存策略 > - 必須告訴瀏覽器哪些東西可以緩存, 哪些不行 > - `cache-control`: 控制緩存內容 > - `expires`: 可以緩存, 但有時效 ## 多進程 > - 多線程: 性能高, 非常複雜 > - 多進程: 性能略低, 相對簡單 > - 相較於單進程, 更充分利用 CPU 資源 > - 主進程: 負責生成子進程, 千萬別幹太多事, 主進程掛了全部就掛了 > - 子進程: 負責做事 > - nodeJS 默認單進程單線程 > - 一般情況, 主進程會等待子進程結束才結束 ### 進程 > - 普通程序無法創建進程, 只有系統進程才能創建進程, 進程是分裂出來的 > - 只有主進程能分裂 > - 分裂出來的兩個進程共享同一套 code ### cluster.fork() > - 分裂進程 ```javascript= const cluster = require(`cluster`); cluster.fork(); // 分裂進程 console.log(1); ``` ```shell $ node 1.js 1 cluster.fork is not a function # 因為只有主進程能分裂, 子進程執行到分裂方法就掛了 ``` ### cluster.isMaster > - 判斷是否為主進程 > - 返回 true 就是主進程 ```javascript= const cluster = require(`cluster`); if (cluster.isMaster){ cluster.fork(); } console.log(1); // shell 1 1 ``` > #### 進程開太多沒有意義, 視核心數極限 ### os.cpus() > - 返回 cpu 資訊 , 用陣列裝 ```javascript= const cluster = require(`cluster`); const os = require(`os`); // console.log(os.cpus()) // [{...},{...},{...},{...}] => 四核 if (cluster.isMaster) { for (let i = 0; i < os.cpus().length; i++) { cluster.fork(); } console.log(`主進程`); } else { console.log(`子進程`) } /* shell 主進程 子進程 子進程 子進程 子進程 */ ``` > #### 父子進程共享[handle](https://zh.wikipedia.org/wiki/%E5%8F%A5%E6%9F%84) > - 一個端口只能有一個程序 > - 可是父子進程可以共享同個端口 ```javascript= const http = require(`http`); http.createServer().listen(5566); http.createServer().listen(5566); /* shell Error: address already in use :::5566 */ // 端口只能單一程序 ``` ```javascript= const cluster = require(`cluster`); const os = require(`os`); const http = require(`http`); if (cluster.isMaster) { for (let i = 0; i < os.cpus().length; i++) { cluster.fork(); } console.log(`主進程`); } else { http.createServer().listen(5566); console.log(`5566`); } /* shell 主進程 5566 5566 5566 5566 */ // 父子進程可以共享同個端口~ ``` ### process.pid > - 進程號碼 ```javascript= const cluster = require(`cluster`); const os = require(`os`); const http = require(`http`); const process = require(`process`); if (cluster.isMaster){ for (let i=0; i<os.cpus().length; i++) { cluster.fork() } console.log(`主進程`) } else { http.createServer((req,res)=>{ console.log(process.pid); // 進程號碼~~~ res.write(`123`); res.end(); }).listen(5566); console.log(`子進程`); } /* shell * * 頁面刷幾次都是 9230 * 因為多核進程運作是第一個滿了才會啟用第二個, 不是分工 * 因為切換進程需要消耗不少資源 * 資源的調度是由系統完成的 * * node 1.js 主進程 子進程 子進程 子進程 子進程 9230 9230 9230 9230 9230 9230 */ ```