# 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
```
> - 壓縮前後
> 
> 
> - html 非常微小的變大, 我猜是表頭多丟了幾個字( `setHeader()` )
> - 如果沒有在表頭告訴瀏覽器編碼方法, 瀏覽器根本不認識這檔案,
> 然後就會跳出下載, 叫使用者自己打開來看
> <img src='https://i.imgur.com/Sc9UQPZ.png' style='width: 200px'>
> - 參考 Yahoo 的 Response Header
> - `content-encoding: gzip`
> - 設置後, 主流的瀏覽器就會自己解壓了
> 
> #### `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 叫瀏覽器讀取自己內存就好了
> - 兩者差距
> 
> 
```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
*/
```