本文解決的問題是目前流行的 Android/IOS 原生應用內嵌 WebView 網頁時,原生與H5頁面登陸狀態的同步。html
大多數混合開發應用的登陸都是在原生頁面中,這就牽扯到一個問題,如何把登陸狀態傳給H5頁面呢?總不能打開網頁時再從網頁中登陸一次系統吧… 兩邊登陸狀態的同步是必須的。前端
100
多位經驗豐富的開發者參與,在 Github 上得到了近1000
個star
的全棧全平臺開源項目想了解或參與嗎?
項目地址:github.com/cachecats/c…vue
其實同步登陸狀態就是把登陸後服務器返回的 token
、userId
等登陸信息傳給H5網頁,在發送請求時將必要的校驗信息帶上。只不過純H5開發是本身有一個登陸頁,登陸以後保存在 Cookie 或其餘地方;混合開發中H5網頁本身不維護登陸頁,而是由原生維護,打開 webview 時將登陸信息傳給網頁。java
實現的方法有不少,能夠用原生與 JS 的通訊機制把登陸信息發送給H5,關於原生與 JS 雙向通訊,我以前寫了一篇詳解文章,不熟悉的同窗能夠看看:android
Android webview 與 js(Vue) 交互git
這裏咱們用另外一種更簡單的方法,經過安卓的 CookieManager
把 cookie
直接寫入 webview 中。程序員
這是安卓開發須要作的。github
先說一下步驟:web
UserInfo
,用來接收服務端返回的數據。UserInfo
格式化爲 json 字符串存入 SharedPreferences
中。SharedPreferences
取出上一步保存的 UserInfo
。Map
將 UserInfo
以鍵值對的格式保存起來,便於下一步保存爲 cookie。UserInfo
中的信息經過 CookieManager
保存到 cookie 中。看似步驟不少,其實就是獲得服務端返回的數據,再經過 CookieManager
保存到 cookie 中這麼簡單,只不過中間須要作幾回數據轉換。vuex
咱們按照上面的步驟一步步看代碼。UserInfo
對象就不貼了,都是些基本的信息。
登陸接口請求成功後,會拿到 UserInfo
對象。在成功回調裏經過下面一行代碼保存 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);
}
/** * 獲取用戶數據 * * @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;
}
}
複製代碼
這裏封裝了一個帶進度條的 ProgressWebviewActivity
,調用時直接打開這個 Activity 並將網頁的 url 地址傳入便可。在 Activity 的 onResume
生命週期方法中執行同步 cookie 的邏輯。爲何在 onResume
中執行?防止App 從後臺切到前臺 webview
從新加載沒有拿到 cookie,可能放在 onCreate
大多數狀況下也沒有問題,但放到 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 存入的數據。
下面是封裝的帶進度條的 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();
//同步 cookie 到 webview
syncCookie(url);
//加載url地址
mWebView.loadUrl(url);
}
/** * 對 webview 進行必要的配置 */
private void settingWebView() {
webSettings = mWebView.getSettings();
//若是訪問的頁面中要與Javascript交互,則webview必須設置支持Javascript
// 若加載的 html 裏有JS 在執行動畫等操做,會形成資源浪費(CPU、電量)
// 在 onStop 和 onResume 裏分別把 setJavaScriptEnabled() 給設置成 false 和 true 便可
webSettings.setJavaScriptEnabled(true);
//設置自適應屏幕,二者合用
webSettings.setUseWideViewPort(true); //將圖片調整到適合webview的大小
webSettings.setLoadWithOverviewMode(true); // 縮放至屏幕的大小
//縮放操做
webSettings.setSupportZoom(true); //支持縮放,默認爲true。是下面那個的前提。
webSettings.setBuiltInZoomControls(true); //設置內置的縮放控件。若爲false,則該WebView不可縮放
webSettings.setDisplayZoomControls(false); //隱藏原生的縮放控件
//其餘細節操做
webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); //沒有網絡時加載緩存
//webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE); //關閉webview中緩存
webSettings.setAllowFileAccess(true); //設置能夠訪問文件
webSettings.setJavaScriptCanOpenWindowsAutomatically(true); //支持經過JS打開新窗口
webSettings.setLoadsImagesAutomatically(true); //支持自動加載圖片
webSettings.setDefaultTextEncodingName("utf-8");//設置編碼格式
//不加的話有些網頁加載不出來,是空白
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這邊的代碼就比較少了,只需在進入頁面時從 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);
}
複製代碼
把這個方法放到儘量早的執行到的頁面的生命週期方法中,好比 created()
、mounted()
、或 activated()
。由於個人頁面中用到了 <keep-alive>
,因此爲了確保每次進來都能拿到信息,把上面的方法放到了 activated()
中。
上面用到了一個工具類 :CookieUtils
,代碼以下:
主要是根據名字取出 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保質期退回一天即可以刪除
},
/** * 獲取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;
}
}
複製代碼
以上就是用最簡單的方法同步安卓原生登陸狀態到H5網頁中的方法。若是你有更便捷的方式,歡迎在評論區交流。
CodeRiver 是一個免費的項目協做平臺,願景是打通 IT 產業上下游,不管你是產品經理、設計師、程序員或是測試,仍是其餘行業人員,只要有好的創意、想法,均可以來 CodeRiver 免費發佈項目,召集志同道合的隊友一塊兒將夢想變爲現實!
CodeRiver 自己仍是一個大型開源項目,致力於打造全棧全平臺企業級精品開源項目。涵蓋了 React、Vue、Angular、小程序、ReactNative、Android、Flutter、Java、Node 等幾乎全部主流技術棧,主打代碼質量。
目前已經有近 100
名優秀開發者參與,github 上的 star
數量將近 1000
個。每一個技術棧都有多位經驗豐富的大佬坐鎮,更有兩位架構師指導項目架構。不管你想學什麼語言處於什麼技術水平,相信都能在這裏學有所獲。
經過 高質量源碼 + 博客 + 視頻
,幫助每一位開發者快速成長。
您的鼓勵是咱們前行最大的動力,歡迎點贊,歡迎送小星星✨ ~