本文解決的問題是目前流行的 Android/IOS 原生應用內(nèi)嵌 WebView 網(wǎng)頁時,原生與H5頁面登錄狀態(tài)的同步。
大多數(shù)混合開發(fā)應用的登錄都是在原生頁面中,這就牽扯到一個問題,如何把登錄狀態(tài)傳給H5頁面呢?總不能打開網(wǎng)頁時再從網(wǎng)頁中登錄一次系統(tǒng)吧… 兩邊登錄狀態(tài)的同步是必須的。
100多位經(jīng)驗豐富的開發(fā)者參與,在 Github 上獲得了近1000個star的全棧全平臺開源項目想了解或參與嗎?
項目地址:https://github.com/cachecats/coderiver
一、同步原理
其實同步登錄狀態(tài)就是把登錄后服務器返回的 token 、userId 等登錄信息傳給H5網(wǎng)頁,在發(fā)送請求時將必要的校驗信息帶上。只不過純H5開發(fā)是自己有一個登錄頁,登錄之后保存在 Cookie 或其他地方;混合開發(fā)中H5網(wǎng)頁自己不維護登錄頁,而是由原生維護,打開 webview 時將登錄信息傳給網(wǎng)頁。
實現(xiàn)的方法有很多,可以用原生與 JS 的通信機制把登錄信息發(fā)送給H5,關于原生與 JS 雙向通信,我之前寫了一篇詳解文章,不熟悉的同學可以看看:
這里我們用另一種更簡單的方法,通過安卓的 CookieManager 把 cookie 直接寫入 webview 中。
二、安卓端代碼
這是安卓開發(fā)需要做的。
先說一下步驟:
- 準備一個對象
UserInfo,用來接收服務端返回的數(shù)據(jù)。 - 登錄成功后把
UserInfo格式化為 json 字符串存入SharedPreferences中。 - 打開 webview 時從
SharedPreferences取出上一步保存的UserInfo。 - 新建一個
Map將UserInfo以鍵值對的格式保存起來,便于下一步保存為 cookie。 - 將
UserInfo中的信息通過CookieManager保存到 cookie 中。
看似步驟很多,其實就是得到服務端返回的數(shù)據(jù),再通過 CookieManager 保存到 cookie 中這么簡單,只不過中間需要做幾次數(shù)據(jù)轉換。
我們按照上面的步驟一步步看代碼。UserInfo 對象就不貼了,都是些基本的信息。
將 UserInfo 保存到 SharedPreferences
登錄接口請求成功后,會拿到 UserInfo 對象。在成功回調(diào)里通過下面一行代碼保存 UserInfo 到 SharedPreferences。
//將UserData存儲到SP
SPUtils.putUserData(context, result.getData());
SPUtils 是操作 SharedPreferences 的工具類,代碼如下。
包含了保存和取出 UserInfo 的方法(代碼中對象名是 UserData),保存時通過 Gson 將對象格式化為 json 字符串,取出時通過 Gson 將 json 字符串格式化為對象。
public class SPUtils {
/**
* 保存在手機里面的文件名
*/
public static final String FILE_NAME = "share_data";
/**
* 存儲用戶信息
*
* @param context
* @param userData
*/
public static void putUserData(Context context, UserData userData) {
SharedPreferences sp = context.getSharedPreferences(FILE_NAME,
Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
Gson gson = new Gson();
String json = gson.toJson(userData, UserData.class);
editor.putString(SPConstants.USER_DATA, json);
SharedPreferencesCompat.apply(editor);
}
/**
* 獲取用戶數(shù)據(jù)
*
* @param context
* @return
*/
public static UserData getUserData(Context context) {
SharedPreferences sp = context.getSharedPreferences(FILE_NAME,
Context.MODE_PRIVATE);
String json = sp.getString(SPConstants.USER_DATA, "");
Gson gson = new Gson();
UserData userData = gson.fromJson(json, UserData.class);
return userData;
}
}
取出 UserInfo 并保存到 cookie 中
這里封裝了一個帶進度條的 ProgressWebviewActivity ,調(diào)用時直接打開這個 Activity 并將網(wǎng)頁的 url 地址傳入即可。在 Activity 的 onResume 生命周期方法中執(zhí)行同步 cookie 的邏輯。為什么在 onResume 中執(zhí)行?防止App 從后臺切到前臺 webview 重新加載沒有拿到 cookie,可能放在 onCreate 大多數(shù)情況下也沒有問題,但放到 onResume 最保險。
@Override
protected void onResume() {
super.onResume();
Logger.d("onResume " + url);
//同步 cookie 到 webview
syncCookie(url);
webSettings.setJavaScriptEnabled(true);
}
/**
* 同步 webview 的Cookie
*/
private void syncCookie(String url) {
boolean b = CookieUtils.syncCookie(url);
Logger.d("設置 cookie 結果: " + b);
}
同步操作封裝到了 CookieUtils 工具類中,下面是 CookieUtils 的代碼:
這個工具類中一共干了三件事,從 SharedPreferences 中取出 UserInfo,將 UserInfo 封裝到 Map 中,遍歷 Map 依次存入 cookie。
public class CookieUtils {
/**
* 將cookie同步到WebView
*
* @param url WebView要加載的url
* @return true 同步cookie成功,false同步cookie失敗
* @Author JPH
*/
public static boolean syncCookie(String url) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
CookieSyncManager.createInstance(MyApplication.getAppContext());
}
CookieManager cookieManager = CookieManager.getInstance();
Map<String, String> cookieMap = getCookieMap();
for (Map.Entry<String, String> entry : cookieMap.entrySet()) {
String cookieStr = makeCookie(entry.getKey(), entry.getValue());
cookieManager.setCookie(url, cookieStr);
}
String newCookie = cookieManager.getCookie(url);
return TextUtils.isEmpty(newCookie) ? false : true;
}
/**
* 組裝 Cookie 里需要的值
*
* @return
*/
public static Map<String, String> getCookieMap() {
UserData userData = SPUtils.getUserData(MyApplication.getAppContext());
String accessToken = userData.getAccessToken();
Map<String, String> headerMap = new HashMap<>();
headerMap.put("access_token", accessToken);
headerMap.put("login_name", userData.getLoginName());
headerMap.put("refresh_token", userData.getRefreshToken());
headerMap.put("remove_token", userData.getRemoveToken());
headerMap.put("unitId", userData.getUnitId());
headerMap.put("unitType", userData.getUnitType() + "");
headerMap.put("userId", userData.getUserId());
return headerMap;
}
/**
* 拼接 Cookie 字符串
*
* @param key
* @param value
* @return
*/
private static String makeCookie(String key, String value) {
Date date = new Date();
date.setTime(date.getTime() + 3 * 24 * 60 * 60 * 1000); //3天過期
return key + "=" + value + ";expires=" + date + ";path=/";
}
}
syncCookie() 方法最后兩行是驗證存入 cookie 成功了沒。
到這里 Android 這邊的工作就做完了,H5可以直接從 Cookie 中取出 Android 存入的數(shù)據(jù)。
ProgressWebviewActivity封裝
下面是封裝的帶進度條的 ProgressWebviewActivity。
/**
* 帶進度條的 WebView。采用原生的 WebView
*/
public class ProgressWebviewActivity extends Activity {
private WebView mWebView;
private ProgressBar web_bar;
private String url;
private WebSettings webSettings;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_web);
url = getIntent().getStringExtra("url");
init();
}
private void init() {
//Webview
mWebView = findViewById(R.id.web_view);
//進度條
web_bar = findViewById(R.id.web_bar);
//設置進度條顏色
web_bar.getProgressDrawable().setColorFilter(Color.RED, android.graphics.PorterDuff.Mode.SRC_IN);
//對WebView進行必要配置
settingWebView();
settingWebViewClient();
//加載url地址
mWebView.loadUrl(url);
}
/**
* 對 webview 進行必要的配置
*/
private void settingWebView() {
webSettings = mWebView.getSettings();
//如果訪問的頁面中要與Javascript交互,則webview必須設置支持Javascript
// 若加載的 html 里有JS 在執(zhí)行動畫等操作,會造成資源浪費(CPU、電量)
// 在 onStop 和 onResume 里分別把 setJavaScriptEnabled() 給設置成 false 和 true 即可
webSettings.setJavaScriptEnabled(true);
//設置自適應屏幕,兩者合用
webSettings.setUseWideViewPort(true); //將圖片調(diào)整到適合webview的大小
webSettings.setLoadWithOverviewMode(true); // 縮放至屏幕的大小
//縮放操作
webSettings.setSupportZoom(true); //支持縮放,默認為true。是下面那個的前提。
webSettings.setBuiltInZoomControls(true); //設置內(nèi)置的縮放控件。若為false,則該WebView不可縮放
webSettings.setDisplayZoomControls(false); //隱藏原生的縮放控件
//其他細節(jié)操作
webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); //沒有網(wǎng)絡時加載緩存
//webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE); //關閉webview中緩存
webSettings.setAllowFileAccess(true); //設置可以訪問文件
webSettings.setJavaScriptCanOpenWindowsAutomatically(true); //支持通過JS打開新窗口
webSettings.setLoadsImagesAutomatically(true); //支持自動加載圖片
webSettings.setDefaultTextEncodingName("utf-8");//設置編碼格式
//不加的話有些網(wǎng)頁加載不出來,是空白
webSettings.setDomStorageEnabled(true);
//Android 5.0及以上版本使用WebView不能存儲第三方Cookies解決方案
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
CookieManager.getInstance().setAcceptThirdPartyCookies(mWebView, true);
webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}
}
/**
* 設置 WebViewClient 和 WebChromeClient
*/
private void settingWebViewClient() {
mWebView.setWebViewClient(new WebViewClient() {
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
Logger.d("onPageStarted");
}
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
Logger.d("onPageFinished");
}
// 鏈接跳轉都會走這個方法
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
Logger.d("url: ", url);
view.loadUrl(url);// 強制在當前 WebView 中加載 url
return true;
}
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
handler.proceed();
super.onReceivedSslError(view, handler, error);
}
});
mWebView.setWebChromeClient(new WebChromeClient() {
@Override
public void onProgressChanged(WebView view, int newProgress) {
super.onProgressChanged(view, newProgress);
Logger.d("current progress: " + newProgress);
//更新進度條
web_bar.setProgress(newProgress);
if (newProgress == 100) {
web_bar.setVisibility(View.GONE);
} else {
web_bar.setVisibility(View.VISIBLE);
}
}
@Override
public void onReceivedTitle(WebView view, String title) {
super.onReceivedTitle(view, title);
Logger.d("標題:" + title);
}
});
}
/**
* 同步 webview 的Cookie
*/
private void syncCookie(String url) {
boolean b = CookieUtils.syncCookie(url);
Logger.d("設置 cookie 結果: " + b);
}
/**
* 對安卓返回鍵的處理。如果webview可以返回,則返回上一頁。如果webview不能返回了,則退出當前webview
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK && mWebView.canGoBack()) {
mWebView.goBack();// 返回前一個頁面
return true;
}
return super.onKeyDown(keyCode, event);
}
@Override
protected void onResume() {
super.onResume();
Logger.d("onResume " + url);
//同步 cookie 到 webview
syncCookie(url);
webSettings.setJavaScriptEnabled(true);
}
@Override
protected void onStop() {
super.onStop();
webSettings.setJavaScriptEnabled(false);
}
}
Activity 的布局文件:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<WebView
android:id="@+id/web_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ProgressBar
android:id="@+id/web_bar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="-7dp"
android:layout_marginTop="-7dp"
android:indeterminate="false"
/>
</RelativeLayout>
上面兩個文件復制過去就能用,進度條的顏色可以任意定制。
三、H5端代碼(Vue實現(xiàn))
相比之下H5這邊的代碼就比較少了,只需在進入頁面時從 cookie 中取出 token 等登錄信息。
其實如果你們后端的校驗是從 cookie 中取 token 的話,前端可以不做任何處理就能訪問成功。
因為其他接口需要用到 userId 等信息,所以在剛進入頁面時從 cookie 取出 UserInfo 并保存到 vuex 中,在任何地方都可以隨時用 UserInfo 啦。
//從Cookie中取出登錄信息并存入 vuex 中
getCookieAndStore() {
let userInfo = {
"unitType": CookieUtils.getCookie("unitType"),
"unitId": CookieUtils.getCookie("unitId"),
"refresh_token": CookieUtils.getCookie("refresh_token"),
"userId": CookieUtils.getCookie("userId"),
"access_token": CookieUtils.getCookie("access_token"),
"login_name": CookieUtils.getCookie("login_name"),
};
this.$store.commit("setUserInfo", userInfo);
}
把這個方法放到盡可能早的執(zhí)行到的頁面的生命周期方法中,比如 created()、mounted()、或 activated()。因為我的頁面中用到了 <keep-alive>,所以為了確保每次進來都能拿到信息,把上面的方法放到了 activated() 中。
上面用到了一個工具類 :CookieUtils,代碼如下:
主要是根據(jù)名字取出 cookie 中對應的值。
/**
* 操作cookie的工具類
*/
export default {
/**
* 設置Cookie
* @param key
* @param value
*/
setCookie(key, value) {
let exp = new Date();
exp.setTime(exp.getTime() + 3 * 24 * 60 * 60 * 1000); //3天過期
document.cookie = key + '=' + value + ';expires=' + exp + ";path=/";
},
/**
* 移除Cookie
* @param key
*/
removeCookie(key) {
setCookie(key, '', -1);//這里只需要把Cookie保質(zhì)期退回一天便可以刪除
},
/**
* 獲取Cookie
* @param key
* @returns {*}
*/
getCookie(key) {
let cookieArr = document.cookie.split('; ');
for (let i = 0; i < cookieArr.length; i++) {
let arr = cookieArr[i].split('=');
if (arr[0] === key) {
return arr[1];
}
}
return false;
}
}
以上就是用最簡單的方法同步安卓原生登錄狀態(tài)到H5網(wǎng)頁中的方法。如果你有更便捷的方式,歡迎在評論區(qū)交流。
全棧全平臺開源項目 CodeRiver
CodeRiver 是一個免費的項目協(xié)作平臺,愿景是打通 IT 產(chǎn)業(yè)上下游,無論你是產(chǎn)品經(jīng)理、設計師、程序員或是測試,還是其他行業(yè)人員,只要有好的創(chuàng)意、想法,都可以來 CodeRiver 免費發(fā)布項目,召集志同道合的隊友一起將夢想變?yōu)楝F(xiàn)實!
CodeRiver 本身還是一個大型開源項目,致力于打造全棧全平臺企業(yè)級精品開源項目。涵蓋了 React、Vue、Angular、小程序、ReactNative、Android、Flutter、Java、Node 等幾乎所有主流技術棧,主打代碼質(zhì)量。
目前已經(jīng)有近 100 名優(yōu)秀開發(fā)者參與,github 上的 star 數(shù)量將近 1000 個。每個技術棧都有多位經(jīng)驗豐富的大佬坐鎮(zhèn),更有兩位架構師指導項目架構。無論你想學什么語言處于什么技術水平,相信都能在這里學有所獲。
通過 高質(zhì)量源碼 + 博客 + 視頻,幫助每一位開發(fā)者快速成長。
項目地址:https://github.com/cachecats/coderiver
您的鼓勵是我們前行最大的動力,歡迎點贊,歡迎送小星星? ~