# NodeJS-2
## 數據庫
> - 關係型數據庫:
> - MySQL, Oracle
> - MySQL 效能高, 安全性高, 容災能力普
> - Oracle
> - 效能高, 安全性高, 容災能力高, 要錢
> - 常用於金融等
> - 數據之間有關係
> - 缺點: 結構固定
> - 大型系統主要使用
> - 文件型數據庫:
> - sqlite
> - 簡單, 小
> - 行動端常用
> - 文檔型數據庫:
> - MongoDB
> - 直接存儲異構數據
> - 方便, 不用為了存儲而改結構
> - NoSQL
> - 沒有複雜的關係
> - 對性能要求很高
> - redis, memcached, hypertable, bigtable
> - 數據倉庫
> - 超大數據
> - 服務端: MySQL, xampp/wamp 裡的 MySQL, ...
> - 客戶端: NodeJS, JAVA, PHP, Navicat For MySQL, ...
> - 庫: 想像成文件夾, 不能存數據, 用來管理表
> - 表: 想像成文件, 存數據用
### 使用 navicat
> - 新建連接服務器
> <img src='https://i.imgur.com/IV1cDlo.png' style='width: 100px'/><img src='https://i.imgur.com/dYdSgd0.png' style='width: 200px'/><img src='https://i.imgur.com/FTYwGUi.png' style='width: 200px'/>
> - 連接服務器:
> 雙擊 <img src='https://i.imgur.com/FTYwGUi.png' style='width: 200px'/> -> <img src='https://i.imgur.com/nhaJ2s2.png' style='width: 200px'/>
> - 新建庫(右鍵 -> new database)
> - 數據庫取名盡量用 `_` 不要用 `-`
> - 字符集常用 `utf8`
> - 校隊集常用 `utf8_general_ci `
> - 雙擊使用該數據庫
> <img src='https://i.imgur.com/gbVIiHM.png' style='width: 200px'/><img src='https://i.imgur.com/ziXgT6O.png' style='width: 200px'/>
> - 導出導入
> - 他導出是用 dump 導出
> 
> - 導出的SQL並沒有創建庫的語句, 所以要導入前要先創庫
> - 運行 SQL 語句
> - 開啟查詢(這東西可以運行SQL語句)
> <img src='https://i.imgur.com/bLfq1YD.png' style='width: 300px'/>
> - 在上面寫一寫後點擊那個三角形(RUN)
> <img src='https://i.imgur.com/bu2pifo.png' style='width: 300px'/><img src='https://i.imgur.com/bYhCFR5.png' style='width: 50px'/>
### MySQL 模塊
> - Q. 能自己寫而不用模塊嗎?
> - 當然可以, 只是非常麻煩, 通常WebServer 跟 MySQLServer 不在同一台機器上, 那麼就需要網路通信, 網絡通信就需要一堆協議, 非常麻煩, 所以沒空就用別人寫好的就行了
#### 安裝模塊
> - 先創建一個 `$ npm init`
> - 查看有無 MySQL 模塊 `$ npm search mysql`
> - 安裝模塊 `$ npm i mysql -D`
#### 基本使用
```javascript=
let mysql = require(`mysql`);
// let db = mysql.createConnection({host: 'localhost', user: 'root', password: 'youknow.', port: '3306', database: 'n_test'});
let db = mysql.createPool({host: 'localhost', user: 'root', password: 'youknow.', port: '3306', database: 'n_test'});
db.query('select * from stu_score;', (err, data)=>{
if (err) {
console.log(err);
} else {
console.log(data);
}
})
```
```shell
% node mysql.js
[
RowDataPacket { ID: 1, NAME: '小明', SUBJECT: '英文', SCORE: 30 },
RowDataPacket { ID: 2, NAME: '大雄', SUBJECT: '國文', SCORE: 90 }
]
^C
# 用 陣列 包 JSON 返回
```
### mysql.createConnection()
`createConnection({host: '電腦', user: 'MySQL 使用者', password: 'MySQL密碼', port: 'MySQL端口', database: '數據庫名稱'})`
> - 缺點: 這不是異步的, 而一台數據庫服務器同時可能要服務多台客戶端, 導致大家都在等待某一台客戶端操作數據庫完成
### mysql.createPool()
`createPool({host: '電腦', user: 'MySQL 使用者', password: 'MySQL密碼', port: 'MySQL端口', database: '數據庫名稱', [maxConnection: 10]})`
> - 連接池, 簡單說就是一開始就對數據庫服務器連接一堆, 放在一個陣列中, 要用就從陣列裡面拿
> - 用法跟 `createConnection` 一樣
> - 可以設定連接數`maxConnection`, 預設 10 個, 通常夠用了
> - 太少連接可能慢
> - 太多的話, 數據庫服務器可能會受不了
> - ps. `host` 可以的話直接寫地址而不要用域名,
> 例如 `localhost` 直接寫 `127.0.0.1`
> 因為使用域名還需要多一個解析域名的動作,直接用地址可以微微的提升效率
### mysql.createConnection().query()
`query('SQL語句', cb(err,data)=>{})`
> - 與數據庫通信是通過網絡進行, 所以他是異步, 所以有回調
> - 一般 SQL 語句的關鍵字 `SELECT` `VALUES` ... 都會習慣大寫, 看起來比較開心
> #### 1251 問題
```shell
$ node mysql.js
...{
code: 'ER_NOT_SUPPORTED_AUTH_MODE',
errno: 1251,
sqlMessage: 'Client does not support authentication protocol requested by server; consider upgrading MySQL client',
sqlState: '08004',
fatal: true
}
```
> - 這是加密問題, 因為新版加密預設都是 `caching_sha2_password`, 而最新的MySQL模塊還沒支援該加密模式, 所以帳密無法在MySQL模塊中使用
```sql
mysql> select host, user, authentication_string, plugin from mysql.user\G
...
*************************** 4. row ***************************
host: localhost
user: root
.....
plugin: caching_sha2_password -- 這
```
> - 解決辦法就是修改密碼, 並指定MySQL 模塊能夠識別的加密方式 `mysql_native_password` 後重登
```sql
mysql> ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密碼';
Query OK, 0 rows affected (0.01 sec)
mysql> flush privileges;
Query OK, 0 rows affected (0.00 sec)
mysql> quit
Bye
----------------------------------------------------------------
mysql> select host, user, authentication_string, plugin from mysql.user\G
...
*************************** 4. row ***************************
host: localhost
user: root
authentication_string: ....
plugin: mysql_native_password -- 改好了
```
> #### 1819 問題
> - 這是密碼不符合設置規則的問題
```sql
mysql> ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'ooxxooxx';
ERROR 1819 (HY000): Your password does not satisfy the current policy requirements
mysql> show variables like 'validate_password%';
+--------------------------------------+--------+
| Variable_name | Value |
+--------------------------------------+--------+
| validate_password.check_user_name | ON |
| validate_password.dictionary_file | |
| validate_password.length | 8 |
| validate_password.mixed_case_count | 1 |
| validate_password.number_count | 1 |
| validate_password.policy | MEDIUM | -- 把這改成 LOW 就行了
| validate_password.special_char_count | 1 |
+--------------------------------------+--------+
7 rows in set (0.01 sec)
mysql> SET GLOBAL validate_password.policy=LOW;
Query OK, 0 rows affected (0.00 sec)
mysql> ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'ooxxooxx';
Query OK, 0 rows affected (0.01 sec)
```
### SQL 注入
> - 假設用戶註冊, 而我寫一個 SQL 語句要使用客戶端送來的帳密來插入數據庫
```javascript=
db.query(`INSERT INTO user_table (id, username, password) VALUES (null, ${user}, ${pass})`, (err, data)=>{})
db.query(`INSERT INTO user_table (id, username, password) VALUES (null, ${user}, ${pass})`, (err, data)=>{})
/* 如果用戶正常傳送數據
/reg?user=abcd&pass=1234
=> db.query(`INSERT INTO user_table ... VALUES (null, 'abcd', '1234')`, (err, data)=>{})
*/
/* 如果用戶惡意傳送
user = '','');drop table user_table;select ''
pass = ''
db.query(`INSERT INTO user_table ... VALUES (null, '', '');drop table user_table;select '','';)`, (err, data)=>{})
=> 然後數據表就被幹掉了~~
*/
```
> - 簡單說就是傳送合法的數據來執行自己想要執行的SQL語句
### 實作: 用戶註冊, 登入
> - 創建數據庫結構
> - 密碼欄最少要給32字符, 因為md5加密後32位, 低於的話寫不進去
```sql
mysql> create table user_login(
-> id int primary key auto_increment,
-> username varchar(20),
-> password varchar(32)
-> ) charset utf8;
Query OK, 0 rows affected, 1 warning (0.02 sec)
```
> - 定義接口
> - JSON 必須雙引號 !!!, 否則`JSON.parse()`會報錯
```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"}
```
> - 首先這個服務器必須提供接口跟靜態文件, 所以我必須判斷是接口還是靜態文件
> - 登入與註冊接口驗證: 校驗, 是否存在等, 是否正確等
> - 密碼最好雙層加密一下
> - 私鑰實際開發時會放在一個文件中, 並有權限控管
> - 靜態文件可以做 流, 壓縮, 緩存 等
> - 先不處理POST
```shell
$ tree
tree
.
├── http.js # 服務器
├── node_modules
│ ...
│ ├── mysql # mysql 模塊
│ │ ├── ...
├── package-lock.json
├── package.json
└── www
├── ajax.html # ajax傳送帳密
└── form.html # 表單傳送帳密
$ cat package.json
{
"name": "nodejs_note",
"version": "1.0.0",
"description": "",
"main": "http.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"mysql": "^2.18.0" # mysql 模塊
}
}
```
```javascript=
const http = require(`http`);
const fs = require(`fs`);
const mysql = require(`mysql`);
const url = require(`url`);
const zlib = require(`zlib`);
// 密碼加密存放用
const crypto = require(`crypto`);
const _key = `dlksfj;lewicw`; // 私鑰, 丟了就可以開始思考跑路的人生規劃
function md5(str) {
let o = crypto.createHash(`md5`);
o.update(str);
return o.digest(`hex`);
}
// 雙層加密
function md5_2(str) {
return md5(md5(str) + _key);
}
// 創建連接池
let db = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'youknow.',
port: 3306,
database: 'n_test'
})
http.createServer((req,res)=>{
// 拿到 GET 的數據並取出我要的參數跟路徑
let {query, pathname} = url.parse(req.url, true);
let {user, pass} = query; // 把兩個參數拿到
// 判斷路徑
// 簡單判斷而已, 如果靜態文件也叫reg或login時, 就有可能有問題
// 這個問題的解法: 在前面多加一層, 例如叫 /API/reg, /STATIC/xxx 之類的就好了
switch (pathname) {
// 接口
case '/reg': // 註冊
// 校驗
if (!user) {
res.write(`{"error": 1, "msg": "GIVE ME USERNAME!"}`);
res.end();
} else if (!pass) {
res.write(`{"error": 1, "msg": "GIVE ME PASSWORD!"}`);
res.end();
} else if (!/^\w{6,10}$/.test(user)) { // 必須限制, 以避免SQL注入
res.write(`{"error": 1, "msg": "Username is Invaild"}`);
res.end();
} else if (/['|"]/.test(pass)) { // 必須限制, 以避免SQL注入
res.write(`{"error": 1, "msg": "PASSWORD is Invaild"}`);
res.end();
} else {
// 查看是否存在
db.query(`SELECT * from user_login where username = '${user}';`, (err, data)=>{
// console.log(`SELECT * from user_login where username = '${user}';`)
if (err) {
console.log(err);
res.write(`{"error": 1, "msg": "DATABASE ERROR"}`);
res.end();
} else if (data.length > 0){
// SELECT 是返回陣列, 如果裡面有長度代表有查到 -> 代表帳號已存在
res.write(`{"error": 1, "msg": "this Username id exsits"}`);
res.end();
} else {
// 如果都沒有問題就可以寫入數據了, 最好使用加密寫入
db.query(`INSERT INTO user_login (id, username, password) VALUES (null, '${user}', '${md5_2(pass)}');`, (err, data)=>{
if (err) {
console.log(err);
res.write(`{"error": 1, "msg": "DATABASE ERROR"}`);
res.end();
} else {
res.write(`{"error": 0, "msg": "SUCCESS"}`);
res.end();
}
})
}
})
}
// res.end(); //=> 寫這會有問題, query 是 callback, 造成 write() 在 end() 後面才寫
break;
case '/login': // 登入接口
// 校驗
if (!user) {
res.write(`{"error": 1, "msg": "GIVE ME USERNAME!"}`);
res.end();
} else if (!pass) {
res.write(`{"error": 1, "msg": "GIVE ME PASSWORD!"}`);
res.end();
} else if (!/^\w{6,10}$/.test(user)) {
res.write(`{"error": 1, "msg": "Username is Invaild"}`);
res.end();
} else if (/['|"]/.test(pass)) {
res.write(`{"error": 1, "msg": "PASSWORD is Invaild"}`);
res.end();
} else {
// 判斷帳號是否存在
db.query(`SELECT * FROM user_login where username = '${user}';`, (err, data)=>{
if (err) {
console.log(err);
res.write(`{"error": 1, "msg": "DATABASE ERROR"}`);
res.end();
} else if (data.length == 0) {
// 長度等於0 代表查不到這個帳號, 代表不存在
res.write(`{"error": 1, "msg": "Username is not exists"}`);
res.end();
} else if (data[0].password != md5_2(pass)) {
// 判斷密碼是否正確
res.write(`{"error": 1, "msg": "Username or Password is incorrect"}`);
res.end();
} else {
res.write(`{"error": 0, "msg": "Success"}`);
res.end();
}
})
}
break;
default:
// 靜態文件: 壓縮, 流, 緩存
let rs = fs.createReadStream(`www${pathname}`);
let gz = zlib.createGzip();
// 緩存 (沒空寫)
res.setHeader(`content-encoding`, `gzip`);
rs.pipe(gz).pipe(res);
rs.on(`error`, err=>{
res.writeHeader(404);
res.write(`NOT FOUND`);
res.end();
})
}
}).listen(5566);
```
```htmlmixed=
<!DOCTYPE html>
<html>
<head>
<meta charset='utf8'>
</head>
<body>
<h2>Registered</h2>
<form action='http://localhost:5566/reg' method='get'>
註冊帳號: <input type='text' name='user'/>
註冊密碼: <input type='text' name='pass'/>
<input type='submit'/>
</form>
<h2>Login</h2>
<form action='http://localhost:5566/login' method='get'>
登入帳號: <input type='text' name='user'/>
登入密碼: <input type='text' name='pass'/>
<input type='submit'/>
</form>
</body>
</html>
```
```htmlmixed=
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<script>
window.onload = function () {
let reg = document.querySelector(`#reg`);
let login = document.querySelector(`#login`);
reg.addEventListener(`click`, e=>{
let user = document.querySelector(`#user`);
let pass = document.querySelector(`#pass`);
// console.log(`${user.value}, ${pass.value}`);
let xhr = new XMLHttpRequest();
// /reg?user=xx&pass=oo
xhr.open(`GET`, `/reg?user=${user.value}&pass=${pass.value}`, 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); // {'error': x, 'msg': 'xx'}
if (data.error) {
console.log(`Error: ` + data.msg);
} else {
console.log(`Success: ` + data.msg);
}
}
}
}
})
login.addEventListener(`click`, e=>{
let user = document.querySelector(`#user`);
let pass = document.querySelector(`#pass`);
let xhr = new XMLHttpRequest();
xhr.open(`get`, `/login?user=${user.value}&pass=${pass.value}`, true);
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {
let data = JSON.parse(xhr.responseText);
if (data.error) {
console.log(`error: ${data.msg}`);
} else {
console.log(`success: ${data.msg}`);
}
}
}
}
})
}
</script>
</head>
<body>
帳號: <input type='text' id='user'/>
密碼: <input type='text' id='pass'/>
<input type='button' value='註冊' id='reg'/>
<input type='button' value='登入' id='login'/>
</body>
</html>
```
## webSocket
> - 雙向
> - 高性能
> 
> - webSocket 不能獨立存在, 必須依附http
> 因為現階段瀏覽器只認 http
> - webSocket 與 http 共享同個端口(類似父子進程的關係)
> - 簡單原理就是瀏覽器與http建立連接後不斷(長連接),
> webSocket 監聽該連接, 有找 ws 就丟給他
> - socket 從 linux 出來後就有了
> websocket 其實就只是 socket 在 web 端實現而已,
> 亦即 webSocket 是前端 (HTML5) 的東西, 後端本來就有 socket
### `socket.io`
> - webSocket 的一個庫
> `npm i socket.io -D`
> - 兼容到IE6
> - Q. 如果自己幹一個要如何兼容雙工?
> - 使用 flash (flash 也有 socket)
> - 瀏覽器通過 `fscommand` 找 flash 要數據
> - flash 與 服務器通信
> - 使用 Eventemmiter
> - `socket.io.listen.on('Name', ()=>{})` 接收
> - `socket.io.listen.emit('NAME', Args)` 發送
> - on 跟 emit 互沒關係, 所以名字ㄧ樣無所謂, 例如收發訊息都取 msg 是可以的
```javascript=
const http = require(`http`);
const io = require(`socket.io`);
let httpServer = http.createServer((req,res)=>{}).listen(5566);
// ws 監聽 http 服務器
let ws = io.listen(httpServer);
// 當有人找 ws 時, 就會觸發這個事件, 並通過傳來的實參通信
ws.on(`connection`, sock=>{
// sock.emit : 發送
// sock.on : 接收
sock.on(`test`, (...args)=>{
console.log(args);
})
// ws 跟 ajax 最大的差別在於 ws 可以主動訪問瀏覽器(雙向)
setInterval(()=>{
sock.emit(`test2`, new Date().getTime()); // 一直傳服務器現在時間煩他
}, 1000)
})
```
```htmlmixed=
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<!-- 客戶端必須先導入socket.io.js 模塊-->
<script src='http://localhost:5566/socket.io/socket.io.js'></script>
<script>
let sock = io.connect(`ws://localhost:5566/`);
// 1. 協議是找 ws 而不是找 http
// 2. ws 跟 http 共享同端口
// io.connect 也有提供 emit 跟 on 來發送接收
sock.emit(`test`, 1,2,3);
sock.on(`test2`, (...args)=>{
console.log(args);
})
</script>
</head>
<body>
</body>
</html>
```
> - Q. 客戶端那個模塊在哪?
> - `listen(httpServer)` 後, 所有請求都會給到 ws 看一下有沒有找 ws 要東西
> 接著看到 `socket.io/socket.io.js` 後就會返回 `client.js` 給瀏覽器
```shell
% tree node_modules/socket.io
node_modules/socket.io
├── LICENSE
├── Readme.md
├── lib
│ ├── client.js # 就是這東西
│ ├── index.js
│ ├── namespace.js
│ ├── parent-namespace.js
│ └── socket.js
└── package.json
```
### 實作超簡易聊天室
#### 服務端
> #### `on('connection', cb)`
> - 連接時觸發
> #### `on('disconnect', cb)`
> - 斷接時觸發
#### 客戶端
> #### `on('connect', cb)`
> - 連接時觸發
> #### `on('disconnect', cb)`
> - 斷接時觸發
> - Q. 如何發給特定客戶端?
> - 給每個連進來的客戶端加上不重複序號, 例如 uuid 或 連上一個就變量 +1 後賦值
```shell
tree
.
├── 1.html # 聊天室頁面
├── node_modules # npm , 裡面有 {"socket.io": "^2.3.0"}
└── ├── ...
├── package-lock.json
├── package.json
└── ws.js # 服務器
```
```javascript=
const http = require(`http`);
const io = require(`socket.io`);
let httpServer = http.createServer((req,res)=>{});
httpServer.listen(5566);
let wsServer = io.listen(httpServer);
// 開一個列表來紀錄連接的客戶端
let aSock = [];
// connection 為定義好的 EventEmitter, 收到連接時觸發,
// 瀏覽器會傳一個實參來對接服務器
wsServer.on(`connection`, sock=>{
// 寫進那個列表
aSock.push(sock);
// 接收訊息, 名字不限
// 使用aaa證明什麼名字都可以
sock.on(`aaa`, str=>{
// 傳送訊息時要過濾掉自己, 聊天自己不會傳給自己
aSock.forEach(s=>{
// 如果不是自己
if (s != sock) {
// 把訊息原封不動傳出去
// 送出的名字也用aaa, 證明兩個互不相干, 可以同名
s.emit(`aaa`, str);
}
})
})
// 客戶端斷接時, 觸發
sock.on(`disconnect`, ()=>{
// 查一下該斷接的客戶端在列表第幾位
let n = aSock.indexOf(sock);
// 砍掉
if (n != -1) { // 做個檢驗的好習慣, 不過正常應該不會 -1
aSock.splice(n, 1);
}
})
})
// 不停監看現在連接有幾個
setInterval(()=>{
console.log(aSock.length);
}, 1000)
```
```htmlmixed=
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<style>
#c_ul {
height: 500px;
width: 500px;
border: 1px solid red;
overflow: auto;
}
#c_ul li.me {
color: blue;
}
.err_connect {
color: red;
text-align: center;
display: none;
}
</style>
<script src='http://localhost:5566/socket.io/socket.io.js'></script>
<script>
let sock = io.connect(`ws://localhost:5566/`);
// 連接服務器時觸發
sock.on(`connect`, ()=>{
console.log(`connect`);
let d_connect = document.querySelector(`.err_connect`);
d_connect.style.display = 'none'; // 關閉警示
c_btn.disabled = false; // 打開按鈕
})
sock.on(`disconnect`, ()=>{
console.log(`disconnect`);
let d_connect = document.querySelector(`.err_connect`);
d_connect.style.display = 'block'; // 打開警示
c_btn.disabled = true; // 關閉按鈕
})
window.onload = function () {
let c_tt = document.querySelector(`#c_tt`);
let c_ul = document.querySelector(`#c_ul`);
let c_btn = document.querySelector(`#c_btn`);
// 點擊後
c_btn.onclick = function () {
// 把訊息傳給服務器
sock.emit(`aaa`, c_tt.value);
// 然後把訊息寫成一個li, 寫到對話筐裡
let li = document.createElement(`li`);
li.innerHTML = c_tt.value;
// 區別出自己的訊息樣式
li.className = 'me';
c_ul.appendChild(li);
c_tt.value = '';
}
// 接收服務器傳來的訊息
sock.on(`aaa`, str=>{
// 寫到對話筐裡
let li = document.createElement(`li`);
li.innerHTML = str;
c_ul.appendChild(li);
})
}
</script>
</head>
<body>
<p class='err_connect'>斷開連接</p> <!-- 警示框 -->
<ul id='c_ul'></ul> <!-- 訊息欄 -->
<textarea id='c_tt' rows="4" cols="50"></textarea> <!-- 打字框 -->
<input id='c_btn' type='button' disabled value='發送'/>
</body>
</html>
```
### 自幹socket
#### 客戶端
> #### `new WebSocket('ws://localhost:<port>/')`
> - JS 真正的 ws 對象
>
> #### 事件
> > #### `onopen`: 連接
> > #### `onmassage`: 收到數據
> > #### `onclose`: 斷接
>
> #### Function
> > #### `send(str)`
> > - 只有這東西, 什麼 emit on 都沒有, 要用要自己裝
> > ```javascript
> > sock.emit = function (name,...args) {
> > sock.send(JSON.stringify({name, 'data': [...args]}));
> > }
> > ```
#### 服務端
> #### net 模塊
> - NodeJS 用來操作 TCP, 原生socket 的模塊
> - 很原生的東西, 所以連接後啥都沒有, 只會給一個 socket 對象實參
> `res` `req` 都沒了,那都是 http 模塊處理好的
> - `http`, `socket.io` 等, 都是`net`模塊的二次封裝
> `events` -> `net` -> `http`
> - `net` 走傳輸層, `http` 走應用層
> `ws` 在前端走應用層, 在後端走傳輸層
>
> #### 事件
> > #### `on('data', cb(data)=>{})` : 收到數據
> > #### `on('end', cb)` : 斷接
> > #### `once('data', cb(data)=>{})`: 收到一次數據就不再觸發
> > - 使用場景:
> > - 接收客戶端的連接請求(握手), 握手只有一次
> > 否則如果使用 `on('data')` 收,
> > 後面接收數據的地方就沒有其他的 Emitter 可以用了
>
> #### Function
> > #### `end()` : 關掉連接
> > #### `write()`
#### Step 1-1. 連接開始(握手)
```javascript=
const net = require(`net`);
// http 模塊已經不能 '直接' 用了在這裡, 因為前端是找 ws, 不是找 http
// http 看到不是找他就直接丟掉
// 要馬找別人用好的 ws 模塊, 例如 socket.io
// 要馬自己用 net 模塊幹一個 wsServer
// net 模塊非常原始, 只會返回一個 socket 對象參數讓雙方通信, 其他啥都沒有
net.createServer(sock=>{
console.log(`連接`);
}).listen(5566);
```
```htmlmixed=
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<script>
// ws 本體
let sock = new WebSocket(`ws://localhost:5566/`);
</script>
</head>
<body>
</body>
</html>
```
> - 此時的確連接上了, 不過查看前端網路狀態
> 
> - 只是一個 \<pending> 而已, 不是真的連接成功
> - 還沒連成功, 當然無法互發數據
#### Step 1-2. 查看握手內容
```javascript=
const net = require(`net`);
net.createServer(sock=>{
console.log(`連接`);
sock.on(`data`, data=>{
console.log(data);
console.log(data.toString());
})
}).listen(5566);
```
```shell
% node server.js
連接
連接
<Buffer 47 45 54 20 ... more bytes> # 一開始進來的是 HTTP 數據, HTTP 先進來的當然是header
GET / HTTP/1.1
Host: localhost:5566
Connection: Upgrade # 升級協議, 找HTTP應該是 keep-alive
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36
Upgrade: websocket # 升級成 ws
Origin: file://
Sec-WebSocket-Version: 13 # ws 版本
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9,zh-TW;q=0.8,zh;q=0.7
Cookie: io=l5eCaCSiVG0Y1wXYAAAC
Sec-WebSocket-Key: gRL/hcG4qf0KJ0tFWvYUlg== # 校驗鑰匙, 這不是加密, 只是簡單的驗證, 隨機字串
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
```
#### Step 1-3. 處理握手數據
> - 1. 第一行沒用, 幹掉,最後兩行 \r\n 也幹掉
> - 2. 利用 `: ` 切開數據, 裝成JSON
> - HTTP 協議規定都是 `: `, 切就對了
> - 很多人都會習慣把 key 全部變成小寫, 減少打錯機會
> - 3. 判斷
> - 3.1 協議內容是不是 ws
> - 3.2 版本對不對
> - 3.3 驗證鑰匙 `SHA1(key+mask) => base64 => client`
> - `Sec-WebSocket-Key` + `258EAFA5-E914-47DA-95CA-C5AB0DC85B11`做 SHA1
> 轉 base64 編碼, 再傳回客戶端
> - 258E... 就是 ws 13 版本規定的, 每個版本不一樣,
> 應該是為了強制服務器升級用的, 沒改就驗證不成功而連不上
> - 回傳客戶端的內容, 注意大小寫
> - `HTTP/版本 101 xxx`
> - `status: 101` 切換協議
> - 後面名字無所謂, 不過還是寫清楚(`Switching Protocols`)好一點
> - `Connection: Upgrade`
> - `Upgrade: websocket`
> - `Sec-WebSocket-Version: 版本號`
> - `Sec-WebSocket-Accept: 轉換後鑰匙`
```javascript=
const net = require(`net`);
const crypto = require(`crypto`); // ws 自帶加密, 必須開加密對象來符合驗證
net.createServer(sock=>{
console.log(`握手開始`);
sock.once(`data`, data=>{
let str = data.toString(); // 頭沒有什麼二進制數據, 直接轉字串就好了
let arr = str.split(`\r\n`);
// console.log(arr);
// = 1. = //
arr = arr.slice(1, arr.length-2);
// console.log(arr);
// = 2. = //
let headers = {};
arr.forEach(line=>{
let [k,v] = line.split(`: `);
headers[k.toLowerCase()] = v;
})
// console.log(headers);
// = 3. = //
// = 3-1 = //
if (headers[`upgrade`] !== `websocket`) {
console.log(`協議為${headers['upgrade']}`);
sock.end(); // 結束連接
// = 3-2 = //
} else if (headers['sec-websocket-version'] !== `13`){
console.log(`ws 版本不是13`);
sock.end();
// = 3-3 = //
} else {
let key = headers['sec-websocket-key'];
let mask = `258EAFA5-E914-47DA-95CA-C5AB0DC85B11`;
let hash = crypto.createHash(`sha1`);
hash.update(key+mask);
let key2 = hash.digest(`base64`);
// console.log(key2);
sock.write(`HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Accept: ${key2}\r\n\r\n`);
console.log(`握手成功`);
// 如果上面都沒問題, 這裡才開始收數據
sock.on(`data`, data=>{
console.log(`收到數據`);
})
}
})
sock.on(`end`, ()=>{
console.log(`斷接`);
})
}).listen(5566);
```
```htmlmixed=
<head>
<meta charset='utf-8'>
<script>
let sock = new WebSocket(`ws://localhost:5566/`);
sock.onopen = function () {
console.log(`連接成功`);
}
sock.onmassage = function () {
console.log(`收到數據`);
}
sock.onclose = function () {
console.log(`連接斷開`)
}
</script>
</head>
```
> 
> - 搞定, 連接時間為 \<Pending>, 因為ws不斷接
#### STEP 2 收發數據(大概流程)
> - 客戶端發送數據不是單純的字符串, 而是`數據幀`(二進制數據)
> - 處理收發來的數據有點太難了, 等未來有機會再找資料學
```javascript=53
...
sock.on(`data`, data=>{
console.log(data);
console.log(data.toString());
})
...
```
```htmlmixed=
<html>
<script>
...
sock.onopen = function () {
console.log(`連接成功`);
sock.send(`GODJJ5566`);
}
...
</script>
</html>
```
```shell
<Buffer 81 89 0b 30 64 3f 4c 7f 20 75 41 05 51 09 3d>
��
0d?L uAQ = # 數據幀直接轉就亂碼了~
```
#### 數據幀的結構
```txt
0 1 2 3 Byte
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 bit
+-+-----+-------+-+-------------+-------------------------------+
|F|R|R|R| Opcode|M| Payload | |
|I|S|S|S| (4) |S| length |Extended payload length (16/64)|
|N|V|V|V| |A| (7) | (if payload len == 126/127) |
| |1|2|3| |K| | |
+-+-----+-------+-+-------------+-------------------------------+
| Extended payload length continued, if payload len == 127 |
+-------------------------------+-------------------------------+
| | Masking-key, if Mask set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------+-------------------------------+
| Payload data continued |
+---------------------------------------------------------------+
| Payload data continued |
+---------------------------------------------------------------+
- 習慣上 32 位系統會用4B作為寬度來標示
- 習慣上 64 位系統會用8B作為寬度來標示
- 這只是習慣標示, 並不是一個幀只佔 4B, 幀的長度是數據多寡決定的
- FIN: 1 bit, 是否為最後一幀
- 如果數據很大, 一幀是傳不完的,
- 所以需要這位來表達是否為最後一幀
- FIN = 1 時, 為最後一幀
- RSV: 3 bit, 預留, 避免沒想到
- Opcode: 4 bit, 幀類型
- 幀類型又有很多種, 1 代表普通幀
- MASK: 1 bit, 掩碼, 是否為加密數據, 默認為1
- Payload length: 7 bit, 乘載資料的長度
- 1111111 為最大值 ==十進制==> 127
- 亦即最長 127 個字節
- Extended payload length
- 如果長度等於 126 Byte, 那就會使用到16(2^16-1)為長度的編碼
- 如果長度等於 127 Byte, 那就會使用到64(2^16-1)為長度的編碼
- Masking-key: 1 or 4 bit 掩碼
- Payload Data: (x+y) Bytes
- Extension data: x Bytes
- Application data: y Bytes
- 解析數據
<Buffer 81 89 0b 30 64 3f 4c 7f 20 75 41 05 51 09 3d>
<Buffer 81 89 3b 35 8d 11 7c 7a c9 5b 71 00 b8 27 0d> # 刷新兩次
| 開始不一樣 |
+-----------+--------------------------+
|MASKING-KEY| Payload DATA |
+-----------+--------------------------+
=> 十六轉二進
81 89 0b 30 64 3f
1000 0001 1000 1001 0000 1011 0011 0000 0110 0100 0011 1111
=> 分析每位代表什麼
81 89 0b 30 64 3f
1 000 0001 1 0001001 00001011001100000110010000111111
F RSV Opcode M payload MASKING-KEY
I A length
N S *註1*
K
- 註1.
- 0001001 ==十進制==> 9 個字節
- GODJJ5566 => 9 Bytes
- 所以最後 9 Bytes 就是 GodJJ5566
- 亦即 <Buffer 4c 7f 20 75 41 05 51 09 3d>,
- 但這是加密過的數據, 需要通過 MASKING-KEY 來把數據解開,
這也是為啥傳相同數據卻看到不同Budder, 因為每次 MASKING-KEY 都不同
- 實作解析數據
- 先判斷是否為最後一幀
- 再判斷數據是否加密
- 接著判斷數據長度
- 然後利用 MASKING-KEY 來解密, 取得數據
- 也就是說如果想要自己幹, 要先學會數據幀的結構, 然後懂的 bit 操作, 接著要學會解密
```
#### 超入門的bit操作
> - `&` 按位與
> - vs. `&&` 邏輯與
> - `1&1=1` `1&0=0` `0&1=0` `0&0=0`
> - Q. `2&9`
> - `00010&1001 = 0000 => 0`
> - 運算時會反過來看, 所以如果我要取
> - FIN: 第1位
> =2=>`&00000001` 而非 `&100000000`
> =16=> `&0x001`
> - Opaode: 第4~7位
> =2=> `&11110000`
> =16=> `&0x0F0`
> - MASK: 第1位
> =2=> `&00000001`
> =16=> `&0x001`
> - PayloadLen: 第1~7位
> =2=> `&11111110`
> =16=> `&0x0FE`
> - 為啥要反過來, 我也不知道, 什麼二進制一直除二的, 看不懂QQ
```javascript=
sock.on(`data`, data=>{
console.log(data);
let FIN = data[0]&0x001; // 取第 1 位
let Opcode = data[0]&0x0F0; // 取第 4~7 位
let MASK = data[1]&0x001;
let PayloadLen = data[1]&0x0FE; // 取第 1~7 位
console.log(FIN, Opcode, MASK, PayloadLen);
})
```
## AJAX2.0
> - 與1.0 的區別
> - formdata: 控制數據, 文件上傳
> - cors 跨域
> - 以前是通過 jsonp 為主, 現在被 ajax2.0 淘汰了
> - websocket?
> - 需要大量修改服務器
> - 有些功能太麻煩, 例如文件上傳
## formdata
> - 模擬 `<form>` 標籤
> - 模擬表單提交
> - 提交文件, 監控上傳進度
> - 屬於AJAX 2.0, 亦即高級瀏覽器才能使用(好像是IE11+)
> - 低級瀏覽器可能要使用 swfuploader(flash)
### 創建及簡單操作
#### new FormData();
> #### `set(k,v)`: 設置 k&v, 同名會覆蓋
> #### `append(k,v)` : 添加 k&v
> #### `get(k)` : 獲取 k&v, 如果值是陣列, 只會拿 \[0]
> #### `getAll(k)` : 獲取 k&v, 且全部都拿來
> #### `delete(k)` : 刪除 k&v
> #### `forEach((item, index, arr)=>{})`
```htmlmixed=
<head>
<meta charset='utf-8'>
<script>
// 創建FD對象
let fd = new FormData();
console.log(fd); // FormData {}
// 設置 k v
fd.set(`aaa`, 123);
console.log(fd); // FormData {}
console.log(fd.get(`aaa`)); // 123
// 設置同名 k 會覆蓋
fd.set(`aaa`, 456);
console.log(fd.get(`aaa`)); // 456
// 添加值
fd.append(`aaa`, 789);
console.log(fd.get(`aaa`)); // 456
// 查看非 str 的值
console.log(fd.getAll(`aaa`)); // (2) ["456", "789"]
// forEach
fd.append(`bbb`, `123`);
fd.forEach((...args)=>{
console.log(...args);
/*
456 aaa FormData {}
789 aaa FormData {}
123 bbb FormData {}
1. 他是存三筆數據, 並沒有把兩個 aaa 合併成一個!
2. 參數是顛倒的, 因為原本就是 forEach((item, index, arr)=>{})
*/
})
// 刪除 k
fd.delete(`aaa`);
console.log(fd.get(`aaa`)); // null
</script>
</head>
```
```htmlmixed=
<head>
<meta charset='utf-8'>
<script>
let fd = new FormData();
fd.set(`aaa`, `123`);
fd.append(`aaa`, `456`);
console.log(fd.getAll(`aaa`));
</script>
</head>
<body>
<form>
<input type='text' name='aaa' value='123' />
<input type='text' name='aaa' value='456' />
</form>
<!-- 基本上 上下兩個 form 是一樣的數據 -->
</body>
```
### 嘗試使用
```htmlmixed=
<head>
<meta charset='utf-8'>
<script>
window.onload = function () {
let ouser = document.querySelector(`#user`);
let opass = document.querySelector(`#pass`);
let obtn = document.querySelector(`#btn`);
obtn.onclick = function () {
let fd = new FormData();
fd.set(`user`, ouser.value);
fd.set(`pass`, opass.value);
let xhr = new XMLHttpRequest();
// GET
/*
// 把數據組成一堆 xx=oo&
// 1. 把 k v 組成 k=v
let arr = []
fd.forEach((value, key)=>{
arr.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
})
// 2. 用 & 把每組數據合起來並傳出去
xhr.open(`GET`, `http://localhost:5566/api?${arr.join('&')}`);
xhr.send();
*/
// POST
xhr.open(`POST`, `http://localhost:5566/api`);
xhr.setRequestHeader(`Content-Type`, `application/x-www-form-urlencoded`);
xhr.send(fd);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {
console.log(`OK`, xhr.responseText);
} else {
console.log(`NOT OK`);
}
}
}
}
}
</script>
</head>
<body>
<form>
<input type='text' id='user'/>
<input type='password' id='pass'/>
<input type='button' id='btn'/>
</form>
</body>
```
```javascript=
const http = require(`http`);
const urllib = require(`url`);
const qs = require(`querystring`);
http.createServer((res, req)=>{
console.log(res.url);
let {pathname: url, query: get} = urllib.parse(res.url, true);
let arr = [];
res.on(`data`, data=>{
arr.push(data);
})
res.on(`end`, ()=>{
console.log(arr);
let post = qs.parse(Buffer.concat(arr).toString()); // 先不管二進制文件
console.log(`*------------*`);
console.log(url, get, post);
})
}).listen(5566);
```
#### 問題: 數據怎麼不是一堆 &?
> - formData 為了考量到上傳文件, 只認 multipart,
> 亦即服務器必須能處理這東西,
> 也亦即 `setRequestHeader()` 在這沒屁用, 因為 formData 根本不屌你
```shell
% node server.js
/api
[
<Buffer 2d 2d 2d 2d 2d 2d 57 65 62 4b 69 74 46 6f 72 6d 42 6f 75 6e 64 61 72 79 33 30 7a 55 72 6b 6c 33 76 46 38 47 65 68 6f 4f 0d 0a 43 6f 6e 74 65 6e 74 2d ... 184 more bytes>
]
*------------*
/api [Object: null prototype] {} [Object: null prototype] {
'------WebKitFormBoundary30zUrkl3vF8GehoO\r\nContent-Disposition: form-data; name': '"user"\r\n' +
'\r\n' +
'GodJJ\r\n' +
'------WebKitFormBoundary30zUrkl3vF8GehoO\r\n' +
'Content-Disposition: form-data; name="pass"\r\n' +
'\r\n' +
'123\r\n' +
'------WebKitFormBoundary30zUrkl3vF8GehoO--\r\n'
}
```
> #### 如果使用一般的表單提交
```htmlmixed=
<body>
<form action='http://localhost:5566/api' method='POST'>
<input type='text' name='user' />
<input type='password' name='pass' />
<input type='submit' />
</form>
</body>
```
```shell
/api
[ <Buffer 75 73 65 72 3d 47 6f 64 4a 4a 26 70 61 73 73 3d 31 32 33> ]
*------------*
/api [Object: null prototype] {} [Object: null prototype] { user: 'GodJJ', pass: '123' }
# 一點問題都沒有
```
### 小結
> - formData 主要是拿來上傳文件的
> - 服務器跟以前的沒有區別, 但要處理 formData , 就必須把處理 multipart 的部分寫好
### 上傳文件
`<input type='file' [multiple]>`
> - HTML5 的屬性 `multiple` : 可以上傳多個文件
> - `input[type=file].value`
> - 以前存放的是真實的文件路徑,
> - 但後來瀏覽器為了隱私, 改成用 fakepath 代替路徑
> - 所以現在是 `磁區/fakepath/filename`
> - `input[type=file].files`
> - 存放的是文件的訊息,
> - 但前台只能看到簡單的訊息
> `name` `lastModified` `lastModifiedDate` `webkitRelativePath` `size` `type`
> - 最後修改時間用來做緩存用的, 跟判斷 304 一樣
> - 其他資訊也可以讓前端人員拿來做東西, 例如
> - GMail: <img src='https://i.imgur.com/TdwrJqk.png' style='width: 300px'/>
> - Outlook: <img src='https://i.imgur.com/xk6aQBm.png' style='width: 300px'/>
> - YahooMail: <img src='https://i.imgur.com/2lpzL7d.png' style='width: 300px'/>
>
> - Q. 大文件上傳
> - 協議是否允許 http=> 1G
> - 用戶體驗考量, 快載完結果斷了會很幹
> - 一般超過一定大小 (ex. 50M) 就可以考慮使用插件了
```htmlmixed=
<head>
<meta charset='utf-8'>
<script>
window.onload = function () {
let of = document.querySelector(`#f1`);
let obtn = document.querySelector(`#btn`);
obtn.onclick = function () {
// input.file 的 value 存的是文件路徑, 並且將路徑用fakepath代替
console.log(of.value);
// C:\fakepath\test.html
console.log(of.files);
// FileList {0: File, 1: File, length: 2}
// length: 2
// 0: File {…}
// name: "test.html"
// lastModified: 1580551850358
// lastModifiedDate: Sat Feb 01 2020 18:10:50 GMT+0800 (Taipei Standard Time) {}
// webkitRelativePath: ""
// size: 1079
// type: "text/html"
// 1: File {…}
// ...
__proto__: FileList
}
}
</script>
</head>
<body>
<form action='javascript:;' method='POST'>
<input type='file' multiple id='f1'/>
<input type='submit' id='btn'/>
</form>
</body>
```
#### formData 傳文件
```htmlmixed=
<head>
<meta charset='utf-8'>
<script>
window.onload = function () {
let ouser = document.querySelector(`#user`);
let opass = document.querySelector(`#pass`);
// 1. 拿標籤
let of = document.querySelector(`#f1`);
let obtn = document.querySelector(`#btn`);
obtn.onclick = function () {
let fd = new FormData();
fd.set(`user`, ouser.value);
fd.set(`pass`, opass.value);
// 2. 把內容添加到 FormData 實例
/* 2.1 問題:
1. set 覆蓋
2. 怎麼知道是兩個檔案, 如果一百個勒?
fd.set(`files`, of.files[0]);
fd.set(`files`, of.files[1]);
*/
// 2.2 問題:
// input[type=file].files 沒有 forEach 方法
// 不過他是偽陣列, 所以... Array.form() 搞定
Array.from(of.files).forEach(file=>{
fd.append(`files`, file);
});
let xhr = new XMLHttpRequest();
// POST
xhr.open(`POST`, `http://localhost:5566/api`);
// 設置表頭沒屁用, 所以幹掉了
xhr.send(fd);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {
console.log(`OK`, xhr.responseText);
} else {
console.log(`NOT OK`);
}
}
}
}
}
</script>
</head>
<body>
<form>
<input type='text' id='user'/><br />
<input type='password' id='pass'/><br />
<input type='file' multiple id='f1'><br />
<input type='button' id='btn'/>
</form>
</body>
```
```javascript=
const http = require(`http`);
const urllib = require(`url`);
const qs = require(`querystring`);
http.createServer((res, req)=>{
console.log(res.url);
let {pathname: url, query: get} = urllib.parse(res.url, true);
let arr = [];
res.on(`data`, data=>{
arr.push(data);
})
res.on(`end`, ()=>{
console.log(arr);
// 應該沒改啥, 只有改這個, 因為傳來的不是 &&&, 就不用解析了
// 純粹看裡面傳了啥就好
let post = Buffer.concat(arr).toString();
console.log(`*------------*`);
console.log(url, get, post);
})
req.end();
}).listen(5566);
```
> - 傳兩個html檔案給服務器
> 
```shell
/api
[
<Buffer ... more bytes>
]
*------------*
/api [Object: null prototype] {} ------WebKitFormBoundaryZI3tz0Ncb0uGXlh5
Content-Disposition: form-data; name="user"
GodJJ
------WebKitFormBoundaryZI3tz0Ncb0uGXlh5
Content-Disposition: form-data; name="pass"
123
------WebKitFormBoundaryZI3tz0Ncb0uGXlh5
Content-Disposition: form-data; name="files"; filename="test.html"
Content-Type: text/html
<!DOCTYPE html>
<html lang='en'>
<...>
</html>
------WebKitFormBoundaryZI3tz0Ncb0uGXlh5
Content-Disposition: form-data; name="files"; filename="test2.html"
Content-Type: text/html
<!DOCTYPE html>
<html>
<...>
</html>
------WebKitFormBoundaryZI3tz0Ncb0uGXlh5--
```
### Express 框架 + body-parser + multer
> - 這裡先用框架搞定那串傳來的東西
> - 以前的框架(如JQ), 大而全, 啥都能幹
> 但是現在的框架通常都只幹一小件事
> 而 express 本身就是幹路由分發
> - 所以使用這些框架時, 往往就需要一些`middleware`(中間件, 插件)來輔助
> - 這裡使用到的 middleware 就是 `body-parser` 跟 `multer`
> - `body-parser` : 處理接收普通 POST 數據
> - `multer` : 處理接收文件 POST 數據
```shell
% npm init -y
% npm i express body-parser multer -D
% cat package.json
{
...,
"devDependencies": {
"body-parser": "^1.19.0",
"express": "^4.17.1",
"multer": "^1.4.2"
}
}
```
#### Express 基礎使用
> - 直接調用 express 模塊對象就創建服務器對象了
> - `listen(<port>)` 服務器不管是誰都需要監聽端口
> #### 接收數據
> `server.get(路徑, cb(req,res)=>{})` `server.post(路徑, cb)` `server.use(路徑, cb)`
> - 使用相應的方法與請求相應的路徑, 就會執行 cb
> - `use()` 是兩個方法通吃
> - Express 使用的是 RESTful 接口風格
> - 利用`請求方式`與`請求路徑`來定義接口, 假設
> - POST 請求 /user => 可能做註冊
> - POST 請求 /user/login => 可能做登入功能
> - GET ...
> - `res.send()` : 響應前端用的, 而且裡面自動搞定 `res.end()`, 別再寫一次
> #### 添加中間件
> `server.use()`
```javascript=
const express = require(`express`);
/*
http.createServer((res,req)=>{}).listen(5566);
*/
let server = express()
server.listen(5566);
// 接收到 GET 請求 '/' 時, 觸發cb
server.get('/', (req, res)=>{
console.log(123);
res.send(`GODJJ`); // express 有的 function, 用來響應
// 注意不用 end(), send() 順便做完了
});
// 接收到 POST 請求 '/' 時, 觸發 cb
server.post('/', (req, res)=>{
console.log(456);
});
// 不管get 還 post 都會觸發
server.use('/', ()=>{
console.log(789);
});
// Q. use 跟 get 誰先觸發?
// 觸發視上下順序寫法而定, 本身沒有誰先誰後問題
```
> - <img src='https://i.imgur.com/US8KxpD.png' style='width: 300px' />
```shell
% node express_server.js
123 # get()
789 # use()
```
#### body-parser
> - 添加時要 `.urlencoded([{options}])`
> - [官方](https://www.npmjs.com/package/body-parser)說的
> - options 裡面很多, 其中
> `extended: <Boolean>` 設置普通模式或擴展模式
#### multer
> - 要先創建一個 multer 對象, 並設置文件存儲路徑
> `multer({dest: '存儲路徑'})`
> - 添加時也可以設置各種模式
> `.any()` 代表所有文件我都要
#### express.static()
> `express.static('靜態資源的路徑')`
> - 這是 express 自帶插件
> - 處理各種靜態資源的事情(壓縮, 緩存...等)
#### 處理上面 formData 數據
```shell
.
├── express_server.js #=> 用 express 寫的服務器
├── node_modules # npm 模塊
│ ├── ...
├── package-lock.json # npm init
├── package.json # npm init
├── server.js # 上面寫的 簡易服務器
├── test2.html # 上面寫的 html 表單
├── upload #=> 想要存放上傳檔案的資料夾
└── www
└── test.html #=> 上面寫的 html formData, 丟到這裡來等等要用
```
```javascript=
const express = require(`express`);
const bodyParser = require(`body-parser`);
const multer = require(`multer`);
// 創建服務器
let server = express()
server.listen(5566);
// 插入 middleware
server.use(bodyParser.urlencoded({extended: false}));
let mObj = multer({dest: './upload/'});
server.use(mObj.any());
// 處理請求
server.post('/api', (req,res)=>{
res.send(`123`);
// 這是 bodyParser 提供給 express 的, 存 普通POST 數據
console.log(req.body);
// 這是 multer 提供給 express 的, 存 文件POST 數據
console.log(req.files);
})
// 如果沒有這東西, 會有跨域的問題, 客戶端會阻止訪問 http
// Access to XMLHttpRequest at 'http://localhost:5566/api' from origin
// 'null' has been blocked by CORS policy:
// No 'Access-Control-Allow-Origin' header is present
// on the requested resource.
// 解決辦法就是讓文件本身也工作在同一個端口就行了, 但是這還要用fs去讀那些有的沒的
// 而 express 提供這個插件來處理靜態文件的麻煩事
server.use(express.static(`./www/`)); // 處理靜態文件就去 www 資料夾找
```
> - 傳文件
> <img src='https://i.imgur.com/P4Yohdz.png' style='width: 300px' />
> - 前端 console
> ```shell
> OK 123
> ```
> - 後端 console
```shell
[Object: null prototype] { user: 'GodJJ', pass: '123' } # 普通POST
[ # 文件 POST
{
fieldname: 'files',
originalname: 'server.js',
encoding: '7bit',
mimetype: 'text/javascript',
destination: './upload/',
filename: 'eb7dd3a59a6c64e39cd64bc85fa9f93a', # MULTER 應該也是用 uuid?
path: 'upload/eb7dd3a59a6c64e39cd64bc85fa9f93a',
size: 467
},
{
fieldname: 'files',
originalname: 'test2.html',
encoding: '7bit',
mimetype: 'text/html',
destination: './upload/',
filename: '631384b4c07d9ac9439dade695b13ab8',
path: 'upload/631384b4c07d9ac9439dade695b13ab8',
size: 512
}
]
% cat upload/631384b4c07d9ac9439dade695b13ab8
<!DOCTYPE html>
<html>
<...>
</html>
% cat upload/eb7dd3a59a6c64e39cd64bc85fa9f93a
const http = require(`http`);
const urllib = require(`url`);
const qs = require(`querystring`);
...
% # 文件一切正常
```
> - 傳圖片
> <img src='https://i.imgur.com/7tsBsqH.png' style='width: 300px' />
> - 05.39 就是右邊這張 <img src='https://i.imgur.com/li8LLbf.png' style='width: 200px' />
```shell
[Object: null prototype] { user: 'GodJJ', pass: '123' }
[
{
fieldname: 'files',
originalname: 'Screen Shot 2020-02-02 at 00.05.39.png',
encoding: '7bit',
mimetype: 'image/png',
destination: './upload/',
filename: '7f43be2fe2fa4e2943b189f508616308',
path: 'upload/7f43be2fe2fa4e2943b189f508616308',
size: 15710
}
]
```
> - 圖片正常 <img src='https://i.imgur.com/pwPAeAp.png' style='width: 300px' />
## 跨域 CORS
> - 下面是一個非常簡單的表單跟只處理GET的服務器
```htmlmixed=
<head>
<meta cahrset='utf-8'>
<script>
window.onload = function () {
let obtn = document.querySelector(`#btn`);
obtn.onclick = function () {
let ouser = document.querySelector(`#user`);
let opass = document.querySelector(`#pass`);
let fd = new FormData();
fd.set(`user`, ouser.value);
fd.set(`pass`, opass.value);
let arr = [];
fd.forEach((v,k)=>{
arr.push(`${encodeURIComponent(k)}=${encodeURIComponent(v)}`);
});
let xhr = new XMLHttpRequest();
xhr.open(`GET`, `http://localhost:5566/api?${arr.join('&')}`, true);
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {
console.log(xhr.responseText);
} else {
console.log(`NOT OK`)
}
}
}
}
}
</script>
</head>
<body>
<input type='text' id='user'><br/>
<input type='password' id='pass'><br/>
<input type='button' id='btn'>
</body>
```
```javascript=
const http = require(`http`);
const urllib = require(`url`);
http.createServer((req,res)=>{
let {pathname: url, query: get} = urllib.parse(req.url, true);
console.log(url, get);
res.write(`123`);
res.end();
}).listen(5566);
```
> - 當我在文件域與5566通信時
> 
> - 前台 console
> ```shell
> Access to XMLHttpRequest at 'http://localhost:5566/api?user=GodJJ&pass=123'
> from origin 'null' has been blocked by CORS policy:
> No 'Access-Control-Allow-Origin' header is present
> on the requested resource.
> ```
> - 後台 shell
> ```shell
> /api [Object: null prototype] { user: 'GodJJ', pass: '123' }
> ```
> - 所以後台是收得到資料的,
> - 事實上瀏覽器也收得到響應, 只是他把響應內容藏起來的
> 錯誤訊息後面也寫了, 後台返回的資源裡沒有 `Access-Control-Allow-Origin` 頭
> 反過來說就是需要有這個頭, 瀏覽器才會把返回的東西給前臺代碼讀取
> - 也就是說事實上不存在跨域問題, 而是瀏覽器對返回資源的限制
> 基於安全性考量
### Origin
> - 瀏覽器發請求給服務器時, Headers 會有一個 `Origin` 來表明來歷
> 如果是文件域, 就為 `null`
> - AJAX 1.0 不會發送這個頭, 也就無法做驗證,
> 簡單的解法就是自己自定義頭給服務器判斷
> - AJAX 2.0 的 CORS 其實就是這個頭
```javascript=
...
http.createServer((req,res)=>{
console.log(req.headers);
...
}
```
```shell
# 文件域訪問時
{
host: 'localhost:5566',
connection: 'keep-alive',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36',
accept: '*/*',
origin: 'null', # null
'sec-fetch-site': 'cross-site',
'sec-fetch-mode': 'cors',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'en-US,en;q=0.9,zh-TW;q=0.8,zh;q=0.7'
}
# 開另外一個本地端口(8080)訪問
{
host: 'localhost:5566',
connection: 'keep-alive',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36',
accept: '*/*',
origin: 'http://localhost:8080', # 告訴你我是 8080
'sec-fetch-site': 'same-site',
'sec-fetch-mode': 'cors',
referer: 'http://localhost:8080/www/',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'en-US,en;q=0.9,zh-TW;q=0.8,zh;q=0.7'
}
# 當然正常來講這地方通常是什麼 https://www.xxx.com 之類的,
# 不過我在本地做實驗沒那東西
```
### Access-Control-Allow-Origin
`{Access-Control-Allow-Origin: 域名 || * || ...}`
> - 服務器就可以依據這個來判斷請求來源自己給不給資源
> - `*` 代表不管是誰全部都給, 不過通常在外層就判斷掉了, 所以內層才給 `*`
```javascript=
const http = require(`http`);
const urllib = require(`url`);
http.createServer((req,res)=>{
console.log(req.headers.origin);
// 判斷請求來源是不是文件域或者本地端口
// 常見判斷 https://www.xxx.com,
// 如果域名有多個, 可以用正則,
// 如果有超多個, 可以塞到 [] 裡 循環判斷
// 不過本地測試就不弄了
// if (/^https?:\/\/(\w+\.)+xxx\.com/.test(req.headers.origin))
if (req.headers.origin == `null` || req.headers.origin.startsWith(`http://localhost`)) {
// 是的話就給頭
res.setHeader(`Access-Control-Allow-Origin`, `*`);
}
let {pathname: url, query: get} = urllib.parse(req.url, true);
console.log(url, get);
res.write(`123`);
res.end();
}).listen(5566);
```
> #### express 也是
```javascript=
...
server.get('/api', (req,res)=>{
if (req.headers.origin == 'null') {
res.setHeader(`Access-Control-Allow-Origin`, `*`);
};
...
```
## 拖曳上傳
### 拖曳事件
#### `ondragenter`: 拖曳進入
#### `ondragleave` : 拖曳離開
#### `ondragover` : 拖曳懸停
> - 白話就是只要拖曳進入後還沒鬆手且還沒離, 就會一直觸發
#### `ondrop` : 拖曳鬆手
> - 拖曳進入後鬆手即觸發
> - 注意! `ondrogover` 沒有阻止默認事件的話, `ondrop` 並不會觸發
> - `ondrop` 的默認事件為開啟文件, 如不想開啟, 必須在最後阻止
> #### `event.dataTransfer.files`
> - 事件對象裡有個 `dataTransfer`, 用來紀錄各種傳輸相關數據
> - `dataTransfer.files` 用來記錄文件資料, 跟 `<input type='file'>.files` 一樣
```htmlmixed=
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<style>
div {
line-height: 100px;
width: 200px;
background: skyblue;
text-align: center;
}
</style>
<script>
window.onload = function () {
let box = document.querySelector(`div`);
// 拖曳碰到盒子時觸發
box.ondragenter = function () {
box.innerHTML = '請鬆手';
}
// 拖曳離開盒子時觸發
box.ondragleave = function () {
box.innerHTML = '拖曳文件';
}
// 拖曳進入盒子且未鬆手時觸發
box.ondragover = function () {
console.log(123);
// 必須阻止默認事件, 才能觸發 ondrop
return false
}
// 拖曳進入盒子後鬆手時觸發
box.ondrop = function (e) {
console.log(`已鬆手`);
console.log(e.dataTransfer.files);
let data = new FormData();
Array.from(e.dataTransfer.files).forEach(v=>{
data.append(`data`, v);
});
let xhr = new XMLHttpRequest();
xhr.open(`POST`, `http://localhost:5566/api`, true);
xhr.send(data);
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {
console.log(`OK`);
} else {
console.log(`NOT OK`);
}
}
}
// 阻止默認事件(開啟文件)
return false
}
}
</script>
</head>
<body>
<div>
拖曳文件
</div>
</body>
</html>
```
```javascript=
// 服務器直接用上面的 express
const express = require(`express`);
const bodyParser = require(`body-parser`);
const multer = require(`multer`);
let server = express()
server.listen(5566);
server.use(bodyParser.urlencoded({extended: false}));
let mObj = multer({dest: './upload/'});
server.use(mObj.any());
server.post('/api', (req,res)=>{
if (req.headers.origin == 'null') {
res.setHeader(`Access-Control-Allow-Origin`, `*`);
};
res.send(`123`);
console.log(req.body);
console.log(req.files);
})
server.use(express.static(`./www/`));
```
> - 我把這兩個文件拖曳進這個藍色盒子
> <img src='https://i.imgur.com/1Kr8Jpa.png' style='width: 300px'/>
> - 前端 console
> ```shell
> (191)123 # drogover 的觸發console
> 已鬆手
> FileList {0: File, 1: File, length: 2}
> OK
> ```
> - 後端 console
```shell
% tree upload # 裡面是空的
upload
0 directories, 0 files
% node express_server.js # 開啟服務器
[Object: null prototype] {}
[
{
fieldname: 'data',
originalname: 'server.js',
encoding: '7bit',
mimetype: 'text/javascript',
destination: './upload/',
filename: '7e42de684275eee9f8e17f6e451bbd61',
path: 'upload/7e42de684275eee9f8e17f6e451bbd61',
size: 467
},
{
fieldname: 'data',
originalname: 'test2.html',
encoding: '7bit',
mimetype: 'text/html',
destination: './upload/',
filename: 'fc4d80efeb0b7fd86a783a178e123bd5',
path: 'upload/fc4d80efeb0b7fd86a783a178e123bd5',
size: 512
}
]
^C
% tree upload # 兩個文件傳進來了
upload
├── 7e42de684275eee9f8e17f6e451bbd61
└── fc4d80efeb0b7fd86a783a178e123bd5
```
## 上傳進度
`XMLHttpResponse.upload.onprogress`
> - AJAX2.0 有個 upload 對象, 裡有存有各種上傳的事件
> 而 upload 對象裡有個 `onprogress` , 紀錄上傳進度的各種數值
```javascript=
let a = new XMLHttpRequest();
console.log(a);
/*
XMLHttpRequest {…}
...
upload: XMLHttpRequestUpload
onloadstart: null
onprogress: null #=> 上傳(c->s)進度事件
onabort: null
onerror: null
onload: null
ontimeout: null
onloadend: null
onprogress: null #=> 下載(s->c)進度事件
...
*/
```
### ProgressEvent.total 跟 ProgressEvent.loaded
> - `XMLHttpResponse.upload` 必須在 `send()` 前面,
> 因為 upload 會有另外一個請求 `OPTIONS`, 如果寫在 `send()` 後面,
> 那就沒辦法發 `OPTIONS` 請求了
> 也就是說使用這個對象後會向服務器發兩個請求 `OPTIONS` 跟 `POST`
> - `OPTIONS`
> - 配置服務器用, 以便服務器能與瀏覽器同步
> - 既然會發兩個請求, 那 express 服務器就不能只用 `post()` 來接了,
> 要馬對 `OPTIONS` 另外寫一個, 要馬使用 `use()` 來接
> - `ProgressEvent` 裡面有 total 跟 loaded
> 只要把 loaded / total , 就能製作進度百分比之類的東西
```htmlmixed=
<!DOCTYPE>
<html>
<head>
<meta charset='utf-8'>
<script>
window.onload = function () {
let oF = document.querySelector(`#f1`);
let oBtn = document.querySelector(`#btn`);
oBtn.onclick = function () {
let data = new FormData();
Array.from(oF.files).forEach(v=>{
data.append(`file`, v);
});
// AJAX
let xhr = new XMLHttpRequest();
xhr.open(`POST`, `http://localhost:5566/api`, true);
// 這東西必須寫在 send() 前面, 否則無法觸發 (OPTIONS) 請求發不出去
xhr.upload.onprogress = function (e) {
console.log(e); // 看看事件對象裡面有啥
}
xhr.send(data);
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {
console.log(xhr.responseText);
} else {
console.log(`NOT OK`);
}
}
}
}
}
</script>
</head>
<body>
<input type='file' id='f1' multiple/><br />
<input type='button' id='btn' />
</body>
</html>
```
```javascript=
const express = require(`express`);
const body = require(`body-parser`);
const multer = require(`multer`);
let server = express();
server.listen(5566);
server.use(body.urlencoded({extended: false}));
let mObj = multer({dest: `./upload/`});
server.use(mObj.any());
// 使用 use 來接
server.use(`/api`, (req, res)=>{
console.log(req.method, req.url); // 看請求方法
if (req.headers.origin == 'null' || req.headers.origin.startsWith(`http://localhost`)) {
res.setHeader(`Access-Control-Allow-Origin`, `*`);
}
console.log(req.body);
console.log(req.files);
res.send(`123`);
})
```
> - 隨便上傳個小東西
```javascript=
// 前台
ProgressEvent {…}
isTrusted: true
lengthComputable: true
loaded: 695 // 這
total: 695 // 這
type: "progress"
target: XMLHttpRequestUpload {…}
currentTarget: XMLHttpRequestUpload {…}
eventPhase: 0
bubbles: false
cancelable: false
defaultPrevented: false
composed: false
timeStamp: 5218.51500000048
srcElement: XMLHttpRequestUpload {…}
returnValue: true
cancelBubble: false
path: []
__proto__: ProgressEvent
```
```shell
// 後台
OPTIONS / # 請求 1 => OPTIONS
{}
undefined
POST / # 請求 2 => POST 跟數據
[Object: null prototype] {}
[
{
fieldname: 'file',
originalname: 'test2.html',
encoding: '7bit',
mimetype: 'text/html',
destination: './upload/',
filename: '5307e0648b1d7d2c363e7b5d961d25db',
path: 'upload/5307e0648b1d7d2c363e7b5d961d25db',
size: 512
}
]
```
> - 如果我上傳一個稍微大一點的文件(185MB), 就會多次觸發事件
```javascript=
ProgressEvent {…}
loaded: 11419648
total: 185310435
ProgressEvent {…}
loaded: 23134208
...
loaded: 37797888
loaded: 49856512
loaded: 63684608
loaded: 75841536
loaded: 87277568
loaded: 99434496
loaded: 113295360
loaded: 125747200
loaded: 136511488
loaded: 151027712
loaded: 159416320
...
ProgressEvent {…}
loaded: 172146688
total: 185310435
ProgressEvent {…}
loaded: 185310435
total: 185310435
```
### 實作: 製作進度條
> - 說白了就是 loaded / total 就搞定了
> - `<meter></meter>` HTML5 的新標籤, 就是進度條樣式標籤,
> 不過實在太醜了又不能改, 所以通常自己幹
> - ps. 如果上傳多個文件而分開製作進度條,
> 可以考慮使用 Array.from() 裡的 forEach 來稿
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
.mF {
height: 50px;
width: 200px;
border: 1px solid black;
}
.mS {
height: 100%;
width: 0;
background: blue;
}
</style>
<script>
window.onload = function () {
let oF = document.querySelector(`#f1`);
let oBtn = document.querySelector(`#btn`);
oBtn.onclick = function () {
let data = new FormData();
Array.from(oF.files).forEach(v=>{
data.append(`file`, v);
});
let xhr = new XMLHttpRequest();
xhr.open(`POST`, `http://localhost:5566/api`, true);
xhr.upload.onprogress = function (e) {
console.log(e.loaded / e.total);
/* 太醜了,又不能改樣式
let oM = document.querySelector(`#m1`);
oM.value = e.loaded / e.total;
*/
// 通常大家都自己幹
let oMS = document.querySelector(`.mS`);
oMS.style.width = 100 * e.loaded / e.total + '%';
}
xhr.send(data);
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {
console.log(xhr.responseText);
} else {
console.log(`NOT OK`);
}
}
}
}
}
</script>
</head>
<body>
<!--<meter id='m1'></meter><br />-->
<div class='mF'>
<div class='mS'></div>
</div>
<input type='file' id='f1' multiple/><br />
<input type='button' id='btn' />
</body>
```
### XMLHttpRequest.onprogress
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
</style>
<script>
window.onload = function () {
...
let xhr = new XMLHttpRequest();
xhr.onprogress = function (ev) {
console.log(ev);
}
xhr.open(`POST`, `http://localhost:5566/api`, true);
xhr.send(data);
...
}
</script>
</head>
```
```javascript=
ProgressEvent {…}
loaded: 3
total: 3
// 因為服務器回傳 123 這三個字, 所以總長度為 3
```
```txt
Access-Control-Allow-Origin: *
Connection: keep-alive
Content-Length: 3 # 123
Content-Type: text/html; charset=utf-8
Date: Mon, 03 Feb 2020 01:01:10 GMT
ETag: W/"3-QL0AFWMIX8NRZTKeof9cXsvbvu8"
X-Powered-By: Expressv
```
### 綜合練習
> - 當文件拖曳到瀏覽器頁面中時, 顯示上傳盒
> - 文件需要丟到上傳盒裡才能上傳
> - 上傳時, 跑進度條
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
.box {
line-height: 100px;
width: 200px;
border: 1px solid black;
text-align: center;
display: none;
}
</style>
<script>
window.onload = function () {
let oBox = document.querySelector(`.box`);
// 對 DOM 設定拖曳懸停事件
let timer;
document.addEventListener(`dragover`, function (e) {
clearTimeout(timer);
oBox.style.display = 'block';
// 如果沒有再觸發事件, 每 300 毫秒就把盒子隱藏
timer = setInterval(function () {
oBox.style.display = 'none';
}, 300);
e.preventDefault();
});
oBox.addEventListener(`dragenter`, function () {
oBox.innerHTML = '請鬆手';
});
oBox.addEventListener(`dragleave`, function () {
oBox.innerHTML = '請拖曳到這';
})
oBox.addEventListener(`drop`, function (e) {
// 存檔案
let data = new FormData();
Array.from(e.dataTransfer.files).forEach(v=>{
data.append(`file`, v);
});
// 傳檔案
let xhr = new XMLHttpRequest();
xhr.open(`POST`, `http://localhost:5566/api`, true);
// 跑進度條
xhr.upload.addEventListener(`progress`, (e)=>{
let m = document.querySelector(`meter`);
m.value = e.loaded / e.total;
})
xhr.send(data);
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
if (xhr.status >= 200 && xhr.status < 300 || status == 304) {
console.log(xhr.resquestText);
} else {
console.log(`NOT OK`);
}
}
}
e.preventDefault();
})
}
</script>
</head>
<body>
<meter style='width: 100%'></meter>
<div class='box'> <!-- 上傳盒 -->
請拖曳到這
</div>
</body>
```
## 讀取文件
`new FileReader()`
> - HTML5 的對象
### 基礎使用
```javascript=
console.log(new FileReader());
/*
FileReader {…}
readyState: 0
result: null // 讀取結果
error: null
onloadstart: null
onprogress: null
onload: null // 讀取結束後觸發
onabort: null
onerror: null
onloadend: null
__proto__: FileReader
readyState: (...)
result: (...)
error: (...)
onloadstart: (...)
onprogress: (...)
onload: (...)
onabort: (...)
onerror: (...)
onloadend: (...)
EMPTY: 0
LOADING: 1
DONE: 2
readAsArrayBuffer: ƒ readAsArrayBuffer()
readAsBinaryString: ƒ readAsBinaryString()
readAsText: ƒ readAsText() // 使用文本讀取
readAsDataURL: ƒ readAsDataURL() // 使用 base64 讀取
abort: ƒ abort()
...
*/
```
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
.box {
line-height: 100px;
width: 200px;
border: 1px solid black;
text-align: center;
display: none;
}
</style>
<script>
window.onload = function () {
let oBtn = document.querySelector(`#btn`);
oBtn.onclick = function () {
let oF = document.querySelector(`#f1`);
let data = oF.files[0]; // 獲取檔案
// 創建對象
let reader = new FileReader();
// 最好先添加事件再讀取,
// 避免檔案太小而瞬間讀完, 結果事件還沒綁定的窘境
reader.onload = function () {
console.log(reader.result); // 讀取結果
}
reader.readAsText(data); // 讀取方式
}
}
</script>
</head>
<body>
<input type='file' multiple id='f1' />
<input type='button' id='btn' />
</body>
```
### readAsText(數據)
> - 如果真的讀取文本, 一切都很正常, 但是如果讀的是二進制檔案就有問題
> - 不能設置字符集, 只能使用`網頁`的字符集
```shell
# 文檔結果正常
<!DOCTYPE html>
<html lang="en">
...
</html>
```
```shell
# PNG 檔當然就亂碼
�PNG
IHDR���]Z�DPLTE����C54�SB����<��v��q���������=.���A3�<-+�M�/...
```
### readAsDataURL(數據)
> - 這個是用 base64 編碼
> - 任何文件都能表現成 base64 的形式
> - 特別常用於圖片
#### Base64
> - 好像起源於服務器與瀏覽器的數據傳輸?
> - S <-> C 數據傳輸方法
> - 直接使用二進制
> - base64
> - base64 可以把二進制數據表現成字符串
```shell
# 文檔讀取
data:text/html;base64,PCFET0NUWVBFIGh0bWw+CjxodG1sIGxhbmc9ImVuIj4KPGhlYWQ+CiAgICA8bWV0YSBjaGFyc2V0PSJVVEYtOCI+CiAgICA8bWV0YSBuYW1lPSJ2aWV3cG9ydCIgY29udGVudD0id2lkdGg9ZGV2aWNlLXdpZHRoLCBpbml0aWFsLXNjYWxlPTEuMCI+CiAgICA8bWV0YSBodHRwLWVxdWl2PSJYLVVBLUNvbXBhdGlibGUiIGNvbnRlbnQ9ImllPWVkZ2UiPgogICAgPHRpdGxlPkRvY3VtZW50PC90aXRsZT4KICAgIDxzdHlsZT4KICAgIAogICAgPC9zdHlsZT4KPC9oZWFkPgo8Ym9keT4KICAgIDxtZXRlciB2YWx1ZT0nNTAnIG1pbj0nMCcgbWF4PScxMDAnPjwvbWV0ZXI+CjwvYm9keT4KPC9odG1sPg==
# 圖檔讀取 -> 這一大串就是圖檔本身
data:image/png;base64,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
```
> - 只要能使用地址的地方(src, url,...) 都能直接使用 base64
> <img src='https://i.imgur.com/YVLy09Y.png' style='width: 200px'/>
```htmlmixed=
<body>
<!-- 使用路徑 -->
<img src='a.png' />
<!-- 使用 base64 -->
<img src='data:image/png;base64,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' />
</body>
```
> - 優點:
> - base64 就是檔案本身, 系統直接讀取就行了, 不用還要到路徑找
> 也就是說瀏覽器不會再向服務器多發一個請求來要圖檔
> - 下面這個是上面那個的請求
> 
> - 瀏覽器只有向服務器要了 a.png 的圖檔
> - 所以有些人會在使用小圖標時, 直接使用 base64
> - 缺點:
> - 維護麻煩
> - base64 編碼會比原本體積大
### 實作: 拖曳預覽篩選上傳並存資料至數據庫
> - 拖曳檔案到瀏覽器放, 並提供預覽篩選功能
> - 上傳後將資料存到數據庫中
> - 數據庫的性能與體積直接相關, 亦即越小越好,
> 所以絕對不會存文件本身進去, 而是存文件的名字進去
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
* {
margin: 0;
padding: 0;
}
.box {
line-height: 100px;
width: 200px;
border: 1px solid black;
text-align: center;
display: none;
}
.u1 {
display: flex;
list-style: none;
}
.u1 li {
height: 200px;
width: 200px;
border: 1px solid black;
position: relative;
}
.u1 li img {
height: 100%;
width: 100%;
}
.u1 li a {
position: absolute;
top: 0;
right: 0;
}
</style>
<script>
window.onload = function () {
let oUl = document.querySelector(`.u1`);
let oBox = document.querySelector(`.box`);
let oBtn = document.querySelector(`#btn`);
// 顯示拖曳框
let timer;
document.addEventListener(`dragover`, e=>{
clearTimeout(timer);
oBox.style.display = 'block';
timer = setInterval(function () {
oBox.style.display = 'none';
}, 300);
e.preventDefault();
})
// 清掉DOM拖曳開啟文件預設, 不然有點煩
document.addEventListener(`drop`, e=>{
e.preventDefault();
})
// 更改拖曳框文字
oBox.addEventListener(`dragenter`, function () {
oBox.innerHTML = '請鬆手';
})
oBox.addEventListener(`dragleave`, function () {
oBox.innerHTML = '請拖曳到這';
})
// 拖曳至拖曳框後觸發
oBox.addEventListener(`drop`, function (e) {
// 拿到所有檔案資料
Array.from(e.dataTransfer.files).forEach(file=>{
// 如果檔案不是圖檔類型, 就不繼續操作
if (!file.type.startsWith('image/')) {
return;
}
// 讀取檔案
let reader = new FileReader();
// 讀取完後觸發
reader.onload = function () {
// 創造 li 並塞到 ul(預覽篩選框) 裡
let oLi = document.createElement(`li`);
// 這步很重要! 在每個 li 上存一下文檔資料, 下面上傳要用
oLi.file = file;
oLi.innerHTML = '<img/><a href="javascript:;" class="del">Delete</a>';
// 圖片的 src 就直接塞讀到的 base64
let oImg = oLi.children[0];
oImg.src = this.result;
// 刪除鈕註冊點擊刪除 li 事件
let oA = oLi.children[1];
oA.onclick = function () {
oUl.removeChild(oLi);
}
oUl.appendChild(oLi);
}
// 用 DataURL 讀取檔案
reader.readAsDataURL(file);
});
e.preventDefault();
})
oBtn.onclick = function () {
let data = new FormData();
// 把剛剛寫在 li 的文檔資料拿出來用
Array.from(oUl.children).forEach(li=>{
data.append(`file`, li.file);
});
let xhr = new XMLHttpRequest();
xhr.open(`POST`, `http://localhost:5566/api`, true);
xhr.send(data);
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {
console.log(xhr.responseText);
} else {
console.log(`NOT OK`);
}
}
}
}
}
</script>
</head>
<body>
<!-- 預覽篩選框 -->
<ul class='u1'></ul>
<!-- 上傳鈕 -->
<input type='button' value='Upload' id='btn'/>
<!-- 拖曳框 -->
<div class='box'>
請拖曳到這
</div>
</body>
```
```javascript=
const express = require(`express`);
const body = require(`body-parser`);
const multer = require(`multer`);
const mysql = require(`mysql`);
let db = mysql.createPool({host: 'localhost', port: '3306', user: 'root', password: 'youknow.', database: 'test'});
let server = express();
server.listen(5566);
server.use(body.urlencoded({extended: false}));
let mObj = multer({dest: `./upload/`});
server.use(mObj.any());
server.use(`/api`, (req, res)=>{
if (req.headers.origin == 'null' || req.headers.origin.startsWith(`http://localhost`)) {
res.setHeader(`Access-Control-Allow-Origin`, `*`);
}
// 寫 SQL
// 方法1.: 一條一條寫
// for() {
// db.query();
// }
// 方法2 : 丟到陣列後, 一次寫全部
let arr = [];
req.files.forEach(file=>{
arr.push(`(null,'${file.originalname}','${file.filename}',${Math.floor(Date.now()/1000)})`);
});
// console.log(`INSERT INTO file VALUES ${arr.join(',')}`);
db.query(`INSERT INTO file VALUES ${arr.join(',')};`, (err, data)=>{
if (err) {
res.send(`NOT OK`);
} else {
res.send(`OK`);
}
});
})
```
> - 小結:
> - 讓用戶篩選上傳圖片的方法思路有兩種
> - 在本地篩選完後再一次上傳
> - 真的都上傳了, 用戶刪除再從數據庫找對應的檔案刪掉
### readAsBinaryString(數據)
> - 二進制用 string 形式存儲
> - 看起來跟 `readAsText` 一模一樣...
```shell
# 一般文本 => 正常顯示
const http = require(`http`);
...
}).listen(5566);
# 圖檔 => 一堆亂碼
PNG
IHDRÞãÝ]ZöDPLTEÿÿÿêC54¨SB
ôû¼<ôv¢öqöûºû¸ÿ½ê=.û·êA3ê<-+¦Mé/¢Bé1%¤Ié8'üÂé,þ÷÷0}óCýCúýïîúÜÚìWLé67(zóëQEùÒÐîtlô§¢øÈÅï÷ñ¹ÝÁ3ªAëK>üäãõ³¯îlcð|ù°
þñÕÿý÷ïôþ§VàðäÐèÕÄ`·u=Áèôë¦Ô°7 9U³lq½ò÷¾»ícYñþõàþëÅó!÷¤©÷Þèýýâ¨ý×µËúüÓvüÊXûÂ.²³/ÈäÎJªNʶ%Æ×û®@±7Ë?Ô6£o@â
```
### readAsArrayBuffer(數據);
> - 以二進制數據的形式存儲數據
```shell
# 一般文本
ArrayBuffer(467) {}
[[Int8Array]]: Int8Array(467) [99, 111, 110, 115, 116, …]
[[Uint8Array]]: Uint8Array(467) [99, 111, 110, 115, 116, …]
byteLength: (...)
__proto__: ArrayBuffer
# 圖檔
ArrayBuffer(3288) {}
[[Int8Array]]: Int8Array(3288) [-119, 80, 78, 71, …]
[[Uint8Array]]: Uint8Array(3288) [137, 80, 78, 71, …]
[[Int16Array]]: Int16Array(1644) [20617, 18254, 2573, 2586, …]
[[Int32Array]]: Int32Array(822) [1196314761, 169478669, 218103808, …]
byteLength: (...)
__proto__: ArrayBuffer
```
## 上傳QA
> #### 斷點續傳
> - 普通的 HTML 做不到, 必須通過客戶端來完成
> - 客戶端通過 `Content-Range` 來實現
> - 移動端的瀏覽器也不行, APP 可以
> #### 多次上傳相同文件, 可否實現秒傳?
> - 瀏覽器: 可以, 但是非常麻煩, 必須先想辦法讀取文件內容, 然後才能檢測文件內容
> - 客戶端: 可以使用檢測文件內容(例如做 md5 驗證), 服務器判斷傳過了就給過之類的
>
> #### 過大圖片可以在前端壓縮後才上傳?
> - 前端不具備處理數據的能力
> - 但是前端在上傳時, 有些瀏覽器會自動地做壓縮