本篇將介紹前端本地存儲裏的Web SQL和IndexedDB,經過一個案例介紹SQL的一些概念。javascript
如今要作一個地圖報表,以下圖所示:html
將全部的訂單數據作一個圖表展現,左邊的地圖展現每一個city的成單狀況,右邊的圖形,展現最近7天的成單狀況。因爲後端的數據須要前端作一些解析,如向谷歌請求每一個city的經緯度,因此後端給前端原始的訂單數據,前端進行格式化和歸類展現。另外把原始數據直接放前端,前端處理起來能夠比較靈活,想怎麼展現就怎麼展現,不用每次展現方式變的時候都須要找後端新加接口。前端
可是數據放在前端管理,相應地就會引入一個問題——如何高效地存儲和使用這些數據。最起碼處理起來不要讓頁面卡了。html5
cookie的數據量比較小,瀏覽器限制最大隻能爲4k,而localStorage和sessionStorage適合於小數據量的存儲,firefox和Chrome限制最大存儲爲5Mb,以下火狐的config:java
localStorage是存放在一個本地文件裏面,在筆者的Mac上是放在:mysql
/Users/yincheng/Library/Application Support/Google/Chrome/Default/Local Storage/ http_www.test.com.localstorageweb
用文本編輯器打開這個二進制文件,能夠看到本地存儲的內容:正則表達式
能夠參照控制檯的輸出:算法
若是一個網站要用掉5Mb硬盤空間,那麼打開過一百個網頁就得花500Mb的空間,因此本地存儲localStorage的空間限制得比較小。sql
另外,能夠看到localStorage是以字符串的方式存儲的,存以前要先JSON.stringify變成字符串,取的時候須要用JSON.parse恢復成相應的格式。localStorage適合於比較簡單的數據存放和管理。
後端給我這樣的JSON數據:
[
{"orderId":100314,"userId":379558604617762,"city":"ca","state":"ca","zipcode":"91000","address":"11","price":2698.00,"createTime":1477651308000},
{"orderId":100821,"userId":514694887070560,"city":"San Francisco","state":"CA","zipcode":"94103","address":"251 Rhode Island St #105","price":2182.00,"createTime":1481104358000}
]
我用這些數據去請求它們的經緯度。
這些數據的量比較大,有成百上千甚至幾萬條數據,數據須要複雜的查詢,須要支持:
若是本身管理JSON數據就會比較麻煩,因此這裏嘗試使用Web SQL來管理這些數據。
SQL做用在關係型數據庫上面,什麼是關係型數據庫?關係型數據庫是由一張張的二維表組成的,以下圖所示:
那什麼是SQL呢?SQL是一種操做關係型DB的語言,支持建立表,插入表,修改和刪除等等,還提供很是強大的查詢功能。
常見的關係型數據庫廠商有MySQL、SQLite、SQL Server、Oracle,因爲MySQL是免費的,因此企業通常用MySQL的居多。
Web SQL是前端的數據庫,它也是本地存儲的一種,使用SQLite實現,SQLite是一種輕量級數據庫,它佔的空間小,支持建立表,插入、修改、刪除表格數據,可是不支持修改表結構,如刪掉一縱列,修改表頭字段名等。可是能夠把整張表刪了。同一個域能夠建立多個DB,每一個DB有若干張表,以下圖示意:
以下代碼所示:
使用openDatabase,傳4個參數,指定數據庫大小,若是指定太大,瀏覽器會提示用戶是否容許使用這麼多空間,如Safari的提示:
若是不容許,瀏覽器將會拋異常:
QuotaExceededError (DOM Exception 22): The quota has been exceeded.
這樣就建立了一個數據庫叫order_test,返回了一個db對象,使用這個db對象建立一張表
以下代碼所示:
db.transaction(function(tx){
tx.executeSql(
"create table if not exists order_data(order_id primary key, format_city, lat, lng, price, create_time)", [], null,
function(tx, err){
throw(`execute sql failed: ${err.code} ${err.message}`);
});
});
複製代碼
傳一個回調給db.transaction,它會傳一個SQLTransaction的實例,它表示一個事務,而後調executeSql函數,傳四個參數,第一個參數爲要執行的SQL語句,第二個參數爲選項,第三個爲成功回調函數,第四個爲失敗回調函數,這裏咱們拋一個異常,打印失敗的描述。咱們執行的SQL語句爲:
create table if not exists order_data(order_id primary key, format_city, lat, lng, price, create_time)
複製代碼
意思是建立一張order_data表,它的字段有6個,第一個order_id爲主鍵,主鍵用來標誌這一列,而且不容許有重複的值。
如今往這張表插入數據。
準備好原始數據和對數據作一些處理,以下所示:
var order = {
orderId: 100314, format_city: "New York, NY, USA",
lat: 40.7127837, lng: -74.0059413,
price: 150, createTime: 1473884040000
};
//把時間戳轉成年月日2017-06-08類型的
var date = dataProcess.getDateStr(order.createTime);
複製代碼
而後執行插入:
tx.executeSql(` insert into order_data values(${order.orderId}, '${order.format_city}', ${order.lat}, ${order.lng}, ${order.price}, '${date}')`);
複製代碼
就能夠在瀏覽器控制檯看到剛剛建立的數據庫、表,以下圖所示:
若是把剛剛的那條數據再插入一遍會怎麼樣呢?如刷新一下頁面,它又從新執行。
插入一個重複主鍵,這裏爲id,executeSql的失敗函數將會執行,以下所示:
因此通常id是自動生成的,mysql能夠指定某個整數字段爲auto_increment,而web sql對整數字段不指定也是auto_increment,須要在建立的時候指定當前字段爲integer,以下語句:
create table student(id integer primary key auto_increment, age, score);
複製代碼
做用是建立一張student表,它的id是自動自增的,執行insert插入時會自動生成一個id:
insert into student(grade, score) values(5, 88);
複製代碼
這樣插入幾回,獲得以下表:
能夠看到id由1開始自動增加。常常利用這種自增功能生成用戶的id、訂單的id等等。
上面指定了id爲整型,就不能插入一個字符串的數據,不然會報錯。而若是沒指定,能夠插入數字也能夠插入字符串,固然同一字段最好類型要一致。如mysql、SQL Server等數據庫都是強類型的。
這裏有一個細節須要注意,後端的mysql的id通常採用64位的長整型,這個數最大值爲一個19位數:
9223372036854775807
而JS的最大整數爲一個16位數,大於這個數的值將會是不可靠的,以下圖所示:
所以若是發生這種狀況的話,須要讓後端把ID看成字符串的方式傳給你。這個我在《爲何0.1 + 0.2不等於0.3?》這篇文章裏面作過討論。
把全部的數據都插入以後,獲得以下表:
而後咱們開始作查詢。
a)查出每一個城市的單數和,按日期升序。便於地圖按city展現,能夠執行如下SQL:
select format_city as city, count(order_id) as 'count', sum(price) as amount from order_data group by format_city order by date
複製代碼
結果以下圖所示:
b)而後再查一下最近7天每一天的單數,用於右邊柱狀圖的展現,執行如下SQL:
select date, count(order_id) as 'count', sum(price) as amount from order_data group by date order by date desc limit 0, 7
複製代碼
獲得:
c)查詢某個orderId是否存在,由於數據須要動態更新,例如每兩個小時更新一次,若是有新數據須要去查詢格式化的地址以及經緯度。而每次請求都是拉取所有數據,所以須要找出哪些是新數據。能夠執行:
select order_id from order_data where order_id = ${order.orderId}
複製代碼
若是返回空的結果集,說明這個orderId不存在。
上面是在控制檯執行,在代碼裏面怎麼獲取結果呢,以下圖所示:
某些字段可能會被重複查詢,如order_id,format_city,若是對這些字段作一個索引,那麼能夠提升查詢的效率。
因爲order_id是主鍵,自動會有索引,其它字段須要手動建立一個索引,如對format_city添加一個索引可執行:
create index if not exists index_format_city on order_data(format_city)
複製代碼
爲何建立索引能夠提升查詢效率呢?由於若是沒建索引要找到某個字段等於某個值的數據,須要遍歷全部的數據條項,查找複雜度爲O(N),而創建索引通常是使用二叉查找樹或者它的變種,查找複雜度變成O(logN),mysql是使用的B+樹。有興趣的可繼續查找資料。
另外字符串可以使用哈希變成數字,字符串索引要比數字低效不少。
使用索引的代價是增長存儲空間,下降插入修改的效率。因此索引不能建太多,若是查詢的次數要明顯高於修改那麼創建索引是好的,相反若是某個字段須要被頻繁修改,那可能不太適合創建索引。
SQL支持很是複雜的查詢,能夠聯表查詢、使用正則表達式查詢、嵌套查詢,還能夠寫一個獨立的SQL腳本。
上面的案例,若是不使用SQL,那兩個查詢本身寫代碼篩選數據也能夠實現,可是會比較麻煩,特別是數據量比較大的時候,若是算法寫得很差,就容易有性能問題。而使用DB數據的查詢性能就交給DB。它仍是異步的,不會有堵塞頁面的狀況。
通常來講,存在如下缺點:
在w3c的文檔上,能夠看到:
This document was on the W3C Recommendation track but specification work has stopped. The specification reached an impasse: all interested implementors have used the same SQL backend (Sqlite), but we need multiple independent implementations to proceed along a standardisation path.
大意是說WebSQL現有的實現是基於現成的第三方SQLite,可是咱們須要獨立的實現。火狐也不打算支持。也就是說主要緣由是web sql太過於依賴SQLite,或許W3C可能會在之後從新制訂一套標準。
雖然已經不建議使用了,可是上面仍是花了不少篇幅介紹web sql,主要是由於SQL是通用的,個人主要目的並非要向讀者介紹web sql的API,怎麼使用web sql,而是給讀者介紹一些SQL的核心概念,如怎麼建表,怎麼插入數據,畢竟SQL是通用的,就算再過個幾十年它也很難會過期。
接下來再介紹第二種數據庫非關係型數據庫
非關係型數據庫根據它的存儲特色,經常使用的有:
(1)key-value型,如Redis/IndexedDB,value能夠爲任意數據類型,以下圖所示:
(2)json/document型,如MongoDB,value按照必定的格式,可對value的字段作索引,IndexedDB也支持,以下圖所示:
非關係型數據庫也叫NoSQL數據庫。
NoSQL是Not Only SQL的簡寫,意思爲不只僅是SQL,但其實它和SQL沒什麼關係,只是爲了避免讓人以爲它太異類。它的特色是存儲比較靈活,可是查找沒有像關係型SQL同樣好用。適用於數據量很大,只須要單表key查詢,一致性不用很高的場景。
IndexedDB是本地存儲的第三種方式,它是非關係型數據庫。它的創建數據庫、建表、插入數據等操做以下代碼以下,這裏不進行拆分講解,具體API細節讀者可查MDN等相關文檔。
//建立和打開一個數據庫
var request = window.indexedDB.open("orders", 7);
var db = null;
request.onsuccess = function(event){
db = event.target.result;
//若是order_data表已經存在,則直接插入數據
if(db.objectStoreNames.contains("order_data")){
var orderStore = db.transaction("order_data", "readwrite").objectStore("order_data");
//insertOrders(orderStore);
}
};
request.onupgradeneeded = function(event){
db = event.target.result;
//若是order_data表不存在則建立,並插入數據
if(!db.objectStoreNames.contains("order_data")){
var orderStore = db.createObjectStore("order_data", {keyPath: "orderId"});
insertOrders(orderStore);
}
};
function insertOrders(orderStore){
var orders = orderData.data;
for(var i = 0; i < orders.length; i++){
orderStore.add(orders[i]); //add是一個異步的操做,返回一個IDBRequest,有onsucess
}
}
執行完以後就有了一張order_data的表,以下所示:
![](https://lc-gold-cdn.xitu.io/c48db7dbd30d7cb14ca5.png)
如今要查詢某個orderId的數據,可執行如下代碼:
function query(orderId){
db.transaction("order_data", "readonly") //IDBTransaction
.objectStore("order_data") //IDBObjectStore
.get(orderId) //IDBRequest
.onsuccess = function(event){
var order = event.target.result;
console.log(order)
};
}
複製代碼
結果以下圖所示:
怎麼查詢value字段裏面的數據呢?如要查詢state爲CA的訂單,那麼給state這個字段添加一個索引就能夠查詢 了,以下所示:
這裏就能夠知道,爲何要叫IndexedDB或者索引數據庫了,由於它主要是經過建立索引進行查詢的。
上面只返回了一個結果,可是通常須要獲取所有的結果,就得使用遊標cursor,以下代碼所示:
打印結果以下:
IndexedDB還支持插入json格式不同的數據,以下代碼:
var specilaData = {
orderId: 'hello, world',
text: "goodbye, world"
};
var orderStore = db.transaction("order_data", "readwrite").objectStore("order_data");
orderStore.add(specilaData).onsuccess = function(event){
orderStore.get('hello, world').onsuccess = function(event){
console.log(event.target.result);
};
};
複製代碼
結果以下圖所示:
上面說關係型數據庫不利於橫向擴展,而在通常的非關係型數據庫裏面,每一個數據存儲的類型均可以不同,即每一個key對應的value的json字段格式能夠不一致,因此不存在添加字段的問題,而相同類型的字段能夠建立索引,提升查詢效率。
NoSQL作不了複雜查詢,如上面的案例要按照日期/city歸類的話,須要本身打開一個遊標循環作處理。因此我選擇用Web SQL主要是這個緣由。
WebSQL兼容性以下caniuse所示:
主要是IE和火狐不支持,而IndexedDB的兼容性會好不少:
數據庫的查找,添加等都是異步操做,有時候你可能須要先發個請求獲取數據,而後插入數據,重複N次以後,再查詢數據。例如我須要先一條條地向谷歌服務器解析地址,再插入數據庫,而後再作查詢。在查詢數據以前須要保證數據已經都所有寫到數據庫裏面了,能夠用Promise解決,在保證效率的同時達到目的。以下代碼所示:
談SQL通常會離不開SQL注入的話題,什麼是SQL注入攻擊呢?
假設有個表單,支持用戶查詢本身在某個地方的訂單,以下圖所示:
所寫的SQL語句是這樣的:
select * from order_data where user_id = 514694887070560 and state = '${userData.state}'
複製代碼
userId根據用戶的登錄信息能夠知道,而state則使用用戶傳來的數據,那麼就變成了一道填(song)空(ming)題,以下圖所示:
正常的查詢以下圖所示:
如今進行腳本注入,如我要查一下全部用戶的訂單狀況,以下所示:
select * from order_data where user_id = 514694887070560 and state = 'CA' union select * from order_data where ''='';
複製代碼
後面引號裏面的東西就是我在空格里面填入的東西,它就會拼成一句合法的SQL語句——查詢order_data表的全部數據,結果以下:
因爲數據庫是放在遠程服務器,我怎麼知道你這張表叫作order_data呢?這就須要猜,根據通常的命名習慣,若是order_data不對,那麼對方服務將會返回出錯,那就再換一個,如order/orders等,不斷地猜,通常能夠在較少次數內猜中。
我還猜想有張用戶表,存放着用戶的密碼,要查一下某我的的密碼,執行如下SQL語句:
select * from order_data where user_id = 514694887070560 and state = 'CA' union select order_id, order_data.user_id, price, address, user.password as city, zipcode, state, format_city, date, lat, lng from order_data join user on user.user_id = order_data.user_id and ''='';
複製代碼
結果以下:
第二個city就是那個用戶的密碼,若是數據庫是明文存儲密碼,那就更便利了。
還能夠再作一些增刪改的操做,這個就比查詢其它用戶信息更危險了。那怎麼防止SQL注入呢?
若是字段類型是數字,則沒有注入的風險,而若是字段是字符串則存在。須要把字符串裏面的引號進行轉義把它變成查詢的內容,在引號裏面是使用連在一塊兒的兩個引號表示一個引號。
更常見的是底層框架先把sql語句編譯好,傳進來的字符串只能作爲內容查詢,這種一般是最安全的,就是有時候不太靈活,特別是查詢條件比較多樣時,若是一個條件就寫一句sql仍是挺煩的,而且條件還能夠組合。
若是網站日訪問量太大,一個數據庫服務極可能會扛不住,須要搞幾臺相同的數據庫服務器分擔壓力,可是要保證這幾個數據庫數據一致性。這個有不少解決方案,最簡單的如mysql的replication:
假設線上有3個數據庫,用戶的一個操做寫到了其中的一個數據庫裏面,這個庫就叫主庫master,其它兩個庫叫從庫slave,主庫會把新數據遠程複製到另外兩個從庫。
談到數據庫離不開另一個話題——備份,備份很重要,假設你的網站某一天被攻擊了,一晚上之間幾十萬個用戶的數據沒了,要是找不回來,或者寫了十年的博客全沒了,就真的得一晚上白頭了。例如筆者會不對期地對本身的博客網站作備份:
用wordpress和db的備份文件,能夠在一個小時以內從0恢復整個博客網站。
備份mysql數據庫能夠執行mysqldump的命令,以root用戶的身份:
mysqldump order > order.bak.mysql –u root –p
複製代碼
就能夠把order這個數據庫備份起來,恢復的時候只需執行:
mysql -u root -p < order.bak.mysql
複製代碼
就能夠把order這個數據庫導進來。
綜合以上,本文談到了本地存儲的三種方式:
最主要是分析了關係型數據庫和非關係型數據庫的特色,關係型數據庫是一名老將,而非關係型隨着大數據的產生應運而生,但它又不侷限於在大數據上使用。html5也增長了這兩種類型的數據庫,爲作Web Application作好準備。雖然Web SQL很早前被deprecated,可是隻要你不用支持IE和Firefox仍是能夠用的,它的好處是查詢比較方便,而IndexedDB存儲比較靈活,查詢不方便。說不定在不久的未來會有一種全新的web關係型數據庫出現。如今不少網站都使用IndexedDB存儲它們的數據。
因此能夠二者嘗試學習和使用一下,一方面爲作那種數據驅動類型的網頁提供便利,另外一方面能夠對數據庫的概念有所瞭解,知道後端是如何建表如何查詢數據返回給你的。