「LeanCloud Web 應用開發實踐」系列直播及文章分享持續進行中。
每週二週四晚上 8 點開始,時長預計 45 分鐘。在 「leanCloud通信」 微信公衆號回覆 「公開課」 便可獲取直播連接。html
《LeanCloud Web 應用開發實踐公開課》上期回顧和本期主題介紹。前端
點擊查看完整公開課視頻node
爲了理清 currentUser 的狀態,須要看下不一樣類型的 WEB 應用是如何運做的。git
使用雲引擎 demo 來演示,可使用 todo-demo.leanapp.cn 來作接下來的嘗試,或者本身部署該 demo 應用嘗試(代碼 版本: 1efc44a )。github
這個 demo 是一個典型的服務端渲染的應用。所謂的服務端渲染是指瀏覽器請求服務端的地址或資源時,服務端返回一個 HTML 文檔(一個很大的字符串),瀏覽器收到 HTML 文檔以後,進行渲染並呈現頁面。經過雲引擎的自定義路由很容易實現這樣的 WEB 應用。web
若是單純看請求和響應,以登陸頁面爲例:後端
$ curl -v https://todo-demo.leanapp.cn/users/login
> GET /users/login HTTP/1.1
> Host: todo-demo.leanapp.cn
>
< HTTP/1.1 200 OK
< Content-Type: text/html; charset=utf-8
<
<!DOCTYPE html><html><head><title>用戶登陸</title>...<input type="submit"
value="登陸" class="btn btn-default"><a href="/users/register" class="btn btn-default">註冊</a></div></form></div></body></html>複製代碼
先配置雲引擎 cookieSession中間件 (代碼):api
app.use(AV.Cloud.CookieSession({ secret: '05XgTktKPMkU', maxAge: 3600000, fetchUser: true }));複製代碼
用戶登陸路由的 代碼 以下:跨域
router.post('/login', function(req, res, next) {
var username = req.body.username;
var password = req.body.password;
AV.User.logIn(username, password).then(function(user) {
res.saveCurrentUser(user);
res.redirect('/todos');
}, function(err) {
res.redirect('/users/login?errMsg=' + err.message);
}).catch(next);
});複製代碼
在雲引擎的自定義路由中調用了 AV.User.logIn 的 API,而且調用了 res.saveCurrentUser(user); 來將用戶信息寫入 cookie。瀏覽器
整個請求和響應的流程:
瀏覽器並提交表單的 username 和 password 信息,向服務器發起請求:
curl -v 'https://todo-demo.leanapp.cn/users/login' -H 'content-type: application/x-www-form-urlencoded' --data 'username=zhangsan&password=zhangsan'複製代碼
請求到達雲引擎登陸相關的路由,根據 username 和 password 進行登陸:
var username = req.body.username;
var password = req.body.password;
AV.User.logIn(username, password)複製代碼
res.saveCurrentUser(user);複製代碼
該操做在最終請求響應時, cookieSession 中間件 會將用戶的信息寫入 header 的 Set-Cookie 中。
< HTTP/1.1 302 Found
< Content-Type: text/plain; charset=utf-8
< Location: /todos
< Set-Cookie: avos:sess=eyJfdWlkIjoiNTUxZDJkZTZlNGIwYjM2NzFhZWNmZWIyIiwiX3Nlc3Npb25Ub2tlbiI6ImFjajd3eTgwdDhmdGtpYzRxYzY1ZDNiZDgifQ==; path=/; expires=Tue, 08 Aug 2017 15:49:21 GMT; secure; httponly
< Set-Cookie: avos:sess.sig=TyI_sXTvNa4nUSxByoX3zxWRZ8M; path=/; expires=Tue, 08 Aug 2017 15:49:21 GMT; secure; httponly
<複製代碼
在響應裏多了兩個 Set-Cookie
信息,收到這樣的響應後,瀏覽器會在 cookie 裏寫入這些信息,其中 avos:sess
對應的值是一個 base64 字符串,具體內容是 :{"uid":"551d2de6e4b0b3671aecfeb2","sessionToken":"acj7wy80t8ftkic4qc65d3bd8"}複製代碼
因此標示用戶身份的 sessionToken
信息保存在 cookie 裏。
avos:sess.sig
是一個校驗使用字符串,能夠不關心。cookie 有個特性:每次請求服務器時,會把 cookie 自動添加到請求的 header 中。因此以後再請求該站點的其餘頁面:
curl 'https://todo-demo.leanapp.cn/todos' -H 'cookie: avos:sess=eyJfdWlkIjoiNTUxZDJkZTZlNGIwYjM2NzFhZWNmZWIyIiwiX3Nlc3Npb25Ub2tlbiI6ImFjajd3eTgwdDhmdGtpYzRxYzY1ZDNiZDgifQ==; avos:sess.sig=TyI_sXTvNa4nUSxByoX3zxWRZ8M'複製代碼
當這些請求到達雲引擎應用以後, cookieSession 中間件 會再次起做用,從請求 header
中取出相關的 cookie 並校驗,從中能獲取到登陸用戶的 sessionToken
,而後從存儲服務獲取該用戶的信息(或稱爲判斷 sessionToken
是否有效),並將 user 信息賦值到 request.currentUser
屬性上。
以後,請求會到達具體的自定義路由,此時就能夠從 request.currentUser
獲取發起請求的登陸用戶信息了。
對於服務端渲染的應用:
服務端渲染的應用在用戶體驗方面存在不足,好比一系列表單填寫完成以後一次性提交,此時服務端判斷參數是否有效再響應用戶;還有服務端每次響應整個 HTML 有很大的帶寬浪費。以後出現了 AJAX 技術使得光標離開某個表單項以後,瀏覽器單獨發送請求到服務端直接判斷其有效性並迅速響應;而且每次瀏覽器與服務端通訊都是一些數據結構(JSON 或者 XML)來下降流量,瀏覽器根據數據結果來修改 DOM 結構進行展示。
LeanCloud 將存儲服務以 REST API 的方式提供服務,讓前端(瀏覽器,或移動設備)能夠方便的操做數據,這使得基於 LeanCloud 的應用基本都是先後端分離的。
當前示例使用一些簡單頁面來模擬先後端分離的應用。
請求一個先後端分離的示例(頁面代碼):
$ curl 'https://todo-demo.leanapp.cn/static/page1.html'
<html>
<head>
<script src="//cdn1.lncld.net/static/js/3.0.4/av-min.js"></script>
</head>
<body>
<h1>page1</h1>
<script>
...
console.log('當前登陸用戶:%s', AV.User.current() && AV.User.current().get('username'))
console.log('開始登陸...')
AV.User.logIn('zhangsan', 'zhangsan')
.then(function(user) {
console.log('登陸成功: username: %s, sessionToken: %s', user.get('username'), user._sessionToken)
})
.then(function() {
console.log('當前登陸用戶:%s', AV.User.current() && AV.User.current().get('username'))
...
</script>
</body>
</html>複製代碼
服務端響應了一個頁面,瀏覽器渲染頁面時,會執行 script 部分的腳本,該腳本可能會作大量工做,好比生成或者修改頁面 DOM,並向服務器發請求獲取其餘數據。好比這個示例就在頁面打開以後 3 秒,經過 JS SDK 向服務器發起一個用戶登陸的請求,收到響應後在瀏覽器 console 輸出一些日誌。
使用瀏覽器請求 page1 ,整個流程以下:
var APP_ID = 'kdrt5GNCjojUjiIujawd5A4n-gzGzoHsz';
var APP_KEY = 'Xvxjo6SVUITIqet69q3mudlF';
AV.init({
appId: APP_ID,
appKey: APP_KEY
});複製代碼
setTimeout(function() {
console.log('當前登陸用戶:%s', AV.User.current() && AV.User.current().get('username'))
console.log('開始登陸...')
AV.User.logIn('zhangsan', 'zhangsan')
}, 3000)複製代碼
{
"sessionToken": "u2xtq3dxxvonapqn5uc9snbz7",
"updatedAt": "2017-08-07T14:39:07.619Z",
"objectId": "59887b8b570c350062430143",
"username": "zhangsan",
"createdAt": "2017-08-07T14:39:07.619Z",
"emailVerified": false,
"mobilePhoneVerified": false
}複製代碼
JS SDK 將該信息反序列化構造出AV.User
對象,而後將其保存在瀏覽器 Local Storage
中。經過 JS SDK 的 AV.User.current()
方法獲取當前登陸用戶,本質上就是去 Local Storage
獲取用戶的信息並返回調用方(好比請求 page2 ,頁面代碼):
...
console.log('當前登陸用戶:%s', AV.User.current() && AV.User.current().get('username'))
...複製代碼
雲函數 是運行在雲引擎(服務端)的一個方法,經過 JS SDK 的 AV.Cloud.run 方法能夠很方便的調用。
示例中定義了一個雲函數(代碼):
...
AV.Cloud.define('whoami', function(req, res) {
console.log('whoami:', req.currentUser);
var username = req.currentUser && req.currentUser.get('username');
res.success(username);
});
...複製代碼
在瀏覽器中經過 JS SDK 調用雲函數(請求 page3 ,頁面代碼):
...
AV.Cloud.run('whoami')
.then(function(username) {
console.log('whoami:', username);
})
...複製代碼
瀏覽器請求雲函數流程以下:
經過 JS SDK 調用雲函數,並根據須要傳遞參數(示例中未涉及)。JS SDK 會根據 Local Storage 中的信息在請求的 header 中附加 X-LC-Session ,值爲用戶身份標示 sessionToken。
請求到達雲引擎應用,雲引擎中間件會判斷是否存在 X-LC-Session 的信息,若是有,就使用該值經過存儲服務獲取用戶信息,並賦值給 request.currentUser。
請求進入雲函數相關代碼流程,開發者就能夠獲取到 currentUser 了:
console.log('whoami:', req.currentUser);
var username = req.currentUser && req.currentUser.get('username');
res.success(username);複製代碼
由於使用 LeanCloud 的先後端分離應用,運行應用的域(好比雲引擎的二級域名 abc.leanapp.cn )和提供服務的域(好比 LeanCloud 存儲服務 api.leancloud.cn/1.1/class/T… )不一樣,根據 cookie 的安全策略是不能在不一樣域傳遞 cookie 的。
因此 LeanCloud 的 SDK 會在請求的 header 中攜帶信息讓服務端感知到當前登陸用戶。
基於 LeanCloud 的先後端分離應用:
登陸方式 | 雲引擎自定義路由 | 瀏覽器 JS SDK + REST API(雲函數) |
---|---|---|
保存位置 | cookie | Local Storage |
服務端感知方式 | 經過 cookieSession 中間件 從 cookie 獲取 | 經過雲引擎中間件從 header 獲取 |
與服務端交互方式 | 頁面跳轉或表單提交。由於同域,cookie 自動攜帶 | 經過 JS SDK 操做存儲服務的數據或調用雲函數。由於跨域,cookie 沒法攜帶,使用 header。 |
服務端用戶登陸/登出操做 | 自定義路由中用戶登陸/登出後能夠操做相關 cookie,瀏覽器 cookie 更新,影響後續請求。 | 雲函數中用戶登陸/登出沒有意義,不會改變瀏覽器 Local Storage 的內容,不影響後續瀏覽器對雲函數的請求。 |
相信到這裏,最初提出的疑問能夠解釋了:
在雲引擎登陸了,可是雲函數卻沒有 currentUser
雲引擎自定義路由登陸只改變瀏覽器 cookie,然後續在瀏覽器經過 JS SDK 調用雲函數時,是否攜帶 SessionToken
的信息在 header
中,和 cookie 無關。
在瀏覽器調用 JS SDK 登陸用戶,頁面跳轉時雲引擎中沒有 currentUser
瀏覽器調用 JS SDK 用戶登陸相關的 API 以後,只是 Local Storage
有變化,並在以後的訪問存儲服務或雲函數時會將 sessionToken
攜帶在 header
中,cookie 並沒有變化。而應用頁面跳轉,或者 form 表單提交訪問雲引擎自定義路由時, cookieSession 中間件 沒法從 cookie 中獲取須要的信息。
sessionToken
。sessionToken
,並調用 JS SDK 的 AV.User.become
方法在瀏覽器登陸。在此以後,不論是請求雲引擎自定義路由仍是請求雲函數,都能確保 currentUser 的存在。固然 cookie 還存在過時的問題,不過這裏就不展開討論了。
經過控制雲引擎中間件的 fetchUser 屬性,能夠下降一部分沒必要要的 _User
的查詢請求。
以 AV.Cloud.define API 爲例,當收到雲函數請求時,雲引擎中間件從請求 header
中獲取 sessionToken
信息,而且確認下 fetchUser
屬性的值:
sessionToken
從存儲服務讀取用戶(_User
表)的信息。以後將 sessionToken
和 currentUser
信息複製到 request
的相關屬性上。sessionToken
賦值到 request 的屬性上。也就意味着雲函數中 ```request.currentUser
爲 undefined
。若是雲函數的相關邏輯須要 _User
的其餘信息,好比 username
,那就設置 fetchUser
爲 true
,或者不設置使其保持默認值。
不然,能夠設置 fetchUser
爲 false
,可是須要在全部數據操做(和雲函數調用)時將 sessionToken 加入到請求中:
var query = new AV.Query('Todo');
query.equalTo('status', 0);
query.find({sessionToken: req.sessionToken})複製代碼
若是 req.sessionToken 有效,則存儲服務會根據查詢條件和 ACL 返回適當的信息。
若是 req.sessionToken 無效(過時或僞造),則存儲服務可能由於 ACL 拒絕操做或返回空結果。