個蛋(codeegg)第 723 次推文
作者: 騎著蝸牛闖紅燈
原文: https://juejin.im/post/5cff8c27f265da1bae38f1c1
先簡單介紹一下,Android在4.4之后采用了Chrome內核,所以我們在開發web頁面的時候,es6的語法,css3的樣式等大可放心使用。
WebView自身的一些方法
//方式1. 加載一個網頁:
webView.loadUrl("http://www.google.com/");
//方式2:加載apk包中的html頁面
webView.loadUrl("file:///android_asset/test.html");
//方式3:加載手機本地的html頁面
webView.loadUrl("content://com.android.htmlfileprovider/sdcard/test.html");
正常情況下,在WebView界面,用戶點擊返回鍵是直接退出該頁面的,著當然不是我們想要的,我們想要的是網頁自己的前進和后退,所以下面介紹網頁前進和后退的一些API
//判斷是否可以后退
Webview.canGoBack
//后退網頁
Webview.goBack
//判斷是否可以前進
Webview.canGoForward
//前進網頁
Webview.goForward
// 參數傳負的話表示后退,傳正值的話表示的是前進
Webview.goBackOrForward(int steps)
對返回鍵的監聽,來實現網頁的后退
public boolean onKeyDown(int keyCode, KeyEvent event) {
if ((keyCode==KEYCODE_BACK) && mWebView.canGoBack) {
mWebView.goBack;
return true;
}
return super.onKeyDown(keyCode, event);
}
如何防止WebView內存泄漏
防止內存泄漏的一個原則就是:生命周期長的不要跟生命周期短的玩。
為了防止WebView不造成內存泄漏,
不要在xml里面定義WebView,而是在Activity選中使用代碼去構建,并且Context使用ApplicationContext
在Activity銷毀的時候,先讓WebView加載空內容,然后重rootView中移除WebView,再銷毀WebView,最后置空
override fun onDestroy {
if (webView !=) {
webView!!.loadDataWithBaseURL(, "", "text/html", "utf-8", )
webView!!.clearHistory
(webView!!.parent as ViewGroup).removeView(webView)
webView!!.destroy
webView=
}
super.onDestroy
}
WebSetting和WebViewClient,WebChromeClien
WebSetting
作用:對WebView進行配置和管理
WebSettings webSettings=webView.getSettings;
// 設置可以與js交互,為了防止資源浪費,我們可以在Activity
// 的onResume中設置為true,在onStop中設置為false
webSettings.setJavaScriptEnabled(true);
//設置自適應屏幕,兩者合用
//將圖片調整到適合webview的大小
webSettings.setUseWideViewPort(true);
// 縮放至屏幕的大小
webSettings.setLoadWithOverviewMode(true);
//設置編碼格式
webSettings.setDefaultTextEncodingName("utf-8");
// 設置允許JS彈窗
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
//設置緩存的模式
webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
關于緩存的設置:
當加載 html 頁面時,WebView會在/data/data/包名目錄下生成 database 與 cache 兩個文件夾,請求的 URL記錄保存在 WebViewCache.db,而 URL的內容是保存在 WebViewCache 文件夾下
緩存模式如下:
//LOAD_CACHE_ONLY: 不使用網絡,只讀取本地緩存數據
//LOAD_DEFAULT: (默認)根據cache-control決定是否從網絡上取據。
//LOAD_NO_CACHE: 不使用緩存,只從網絡獲取數據.
//LOAD_CACHE_ELSE_NETWORK,只要本地有,無論是否過期,或no-cache,都使用緩存中的數據。
離線加載
if (NetStatusUtil.isConnected(getApplicationContext)) {
webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);//根據cache-control決定是否從網絡上取數據。
} else {
webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);//沒網,則從本地獲取,即離線加載
}
webSettings.setDomStorageEnabled(true); // 開啟 DOM storage API 功能
webSettings.setDatabaseEnabled(true); //開啟 database storage API 功能
webSettings.setAppCacheEnabled(true);//開啟 Application Caches 功能
String cacheDirPath=getFilesDir.getAbsolutePath + APP_CACAHE_DIRNAME;
webSettings.setAppCachePath(cacheDirPath); //設置 Application Caches 緩存目錄
WebViewClient 作用
處理各種通知,請求事件,主要有,網頁開始加載,記載結束,加載錯誤(如404),處理https請求,具體使用請看下面代碼,注釋清晰。
webView!!.webViewClient=object : WebViewClient {
// 啟用WebView,而不是系統自帶的瀏覽器
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
view.loadUrl(url)
return true
}
// 頁面開始加載,我們可以在這里設置loading
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
tv_start.text="開始加載了..."
}
// 頁面加載結束,關閉loading
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
tv_end.text="加載結束了..."
}
// 只要加載html,js,css的資源,每次都會回調到這里
override fun onLoadResource(view: WebView?, url: String?) {
loge("onLoadResource invoked")
}
// 在這里我們可以加載我們自己的404頁面
override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
loge("加載錯誤:${error.toString}")
}
// webview默認設計是不開啟https的,下面的設置是允許使用https
override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) {
handler?.proceed
}
// js調用Android的方法,在這里可以,該方法不存在通過注解的方式的內存泄漏,但是想拿到Android的返回值的話很難,
// 可以通過Android調用js的代碼的形式來傳遞返回值,例如下面的方式
// Android:MainActivity.java
// mWebView.loadUrl("javascript:returnResult(" + result + ")");
// JS:javascript.html
// function returnResult(result){
// alert("result is" + result);
// }
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
val uri=Uri.parse(request?.url.toString)
// 一般根據scheme(協議格式) & authority(協議名)判斷(前兩個參數)
//假定傳入進來的 url="js://webview?arg1=111&arg2=222"(同時也是約定好的需要攔截的)
if (uri.scheme=="js") {
if (uri.authority=="webview") {
toast_custom("js調用了Android的方法")
val queryParameterNames=uri.queryParameterNames
queryParameterNames.forEach {
loge(it + ":" + uri.getQueryParameter(it))
}
}
return true
}
return super.shouldOverrideUrlLoading(view, request)
}
// 攔截資源 通常用于h5的首頁頁面,將常用的一些資源,放到本地
override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
if(request?.url.toString.contains("logo.gif")){
var inputStream: InputStream?=
inputStream=applicationContext.assets.open("images/test.png")
return WebResourceResponse("image/png","utf-8", inputStream)
}
return super.shouldInterceptRequest(view, request)
}
}
注意:
5.1 以上默認禁止了https和http的混用,下面的設置是開啟:
if (Build.VERSION.SDK_INT >=Build.VERSION_CODES.LOLLIPOP) {
webView.getSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}
WebChromeClient 作用
輔助webview的一下回調方法,可以得到網頁加載的進度,網頁的標題,網頁的icon,js的一些彈框,直接看代碼,注釋清晰。
webView!!.webChromeClient=object : WebChromeClient {
// 網頁加載的進度
override fun onProgressChanged(view: WebView?, newProgress: Int) {
tv_progress.text="$newProgress%"
}
// 獲得網頁的標題
override fun onReceivedTitle(view: WebView?, title: String?) {
tv_title.text=title
}
//js Alert
override fun onJsAlert(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean {
AlertDialog.Builder(this@WebActivity)
.setTitle("JsAlert")
.setMessage(message)
.setPositiveButton("OK") { _, _ -> result?.confirm }
.setCancelable(false)
.show
return true
}
// js Confirm
override fun onJsConfirm(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean {
return super.onJsConfirm(view, url, message, result)
}
//js Prompt
override fun onJsPrompt(
view: WebView?,
url: String?,
message: String?,
defaultValue: String?,
result: JsPromptResult?
): Boolean {
return super.onJsPrompt(view, url, message, defaultValue, result)
}
}
Android和js的交互
Android調用js
1. 通過webview的loadUrl
注意:該方式必須在webview加載完畢之后才能調用,也就是webviewClient的onPageFinished方法回調之后,而且該方法的執行 會刷新界面,效率較低
js代碼:
function callJs{
alert("Android 調用了 js代碼)
}
kotlin代碼:
webView?.loadUrl("javascript:callJs")
2. 通過webview的evaluateJavaScript
比起第一種方法,效率更高,但是要在4.4之后才能使用
js代碼:
function callJs{
// alert("Android 調用了 js代碼)
return {name:'wfq',age:25}
}
kotlin代碼:
webView?.evaluateJavascript("javascript:callJs") {
// 這里直接拿到的是js代碼的返回值
toast(it) // {name:'wfq',age:25}
}
js調用Android
1. 通過webview的addJavaScriptInterface進行對象映射
我們可以單獨定義一個類,所有需要交互的方法可以全部寫在這個類里面,當然也可以直接寫在Activity里面,下面以直接定義在Activity里面為例,優點:使用方便,缺點:存在漏洞(4.2之前),請看下面的“WebView的一些漏洞以及如何防止”
kotlin中定義被js調用的方法
@JavascriptInterface
fun hello(name: String) {
toast("你好,我是來自js的消息:$msg")
}
js代碼
function callAndroid{
android.hello("我是js的,我來調用你了")
}
kotlin中們在webview里面設置Android與js的代碼的映射
webView?.addJavascriptInterface(this, "android")
2. 通過webviewClient的shouldOverrideUrlLoading的回調來攔截url
具體使用:解析該url的協議,如果監測到是預先約定好的協議,那么就調用相應的方法。比較安全,但是使用麻煩,js獲取Android的返回值的話很麻煩,只能通過上面介紹的通過loadurl去執行js代碼把返回值通過參數傳遞回去
首先在js中約定號協議
function callAndroid{
// 約定的url協議為:js://webview?name=wfq&age=24
document.location="js://webview?name=wfq&age=24"
}
在kotlin里面,當loadurl的時候就會回調到shouldOverrideUrlLoading里面
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
val uri=Uri.parse(request?.url.toString)
// 一般根據scheme(協議格式) & authority(協議名)判斷(前兩個參數)
//假定傳入進來的 js://webview?name=wfq&age=24
if (uri.scheme=="js") {
if (uri.authority=="webview") {
toast_custom("js調用了Android的方法")
val queryParameterNames=uri.queryParameterNames
queryParameterNames.forEach {
loge(it + ":" + uri.getQueryParameter(it))
}
}
return true
}
return super.shouldOverrideUrlLoading(view, request)
}
3.通過webChromeClient的onJsAlert,onJsConfirm,onJsPrompt回調來攔截對話框
通過攔截js對話框,得到他們的消息,然后解析即可,為了安全,建議內容采用上面介紹的url協議, 常用的攔截的話就是攔截prompt,因為它可以返回任意值,alert沒有返回值,confirm只能返回兩種類型,確定和取消
js代碼
function clickprompt{
var result=prompt("wfq://demo?arg1=111&arg2=222");
alert("demo " + result);
}
kotlin代碼
override fun onJsPrompt(
view: WebView?,
url: String?,
message: String?,
defaultValue: String?,
result: JsPromptResult?
): Boolean {
val uri=Uri.parse(message)
if (uri.scheme=="wfq") {
if (uri.authority=="demo") {
toast_custom("js調用了Android的方法")
val queryParameterNames=uri.queryParameterNames
queryParameterNames.forEach {
loge(it + ":" + uri.getQueryParameter(it))
}
// 將需要返回的值通過該方式返回
result?.confirm("js調用了Android的方法成功啦啦啦啦啦")
}
return true
}
return super.onJsPrompt(view, url, message, defaultValue, result)
}
由于攔截了彈框,所以js代碼的alert需要處理 這里的message便是上面代碼的返回值通過alert顯示出來的信息
override fun onJsAlert(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean {
AlertDialog.Builder(this@WebActivity)
.setTitle("JsAlert")
.setMessage(message)
.setPositiveButton("OK") { _, _ -> result?.confirm }
.setCancelable(false)
.show
return true
}
上面三種方式的區別:
addJavascriptInterface 方便簡潔,4.0以下存在漏洞,4.0以上通過@JavascriptInterface注解修復漏洞。
WebViewClient.shouldOverrideUrlLoading回調,不存在漏洞,使用復雜,需要定義協議的約束,但是返回值的話有些麻煩,在不需要返回值的情況下可以使用這個方式。
通過WebChromeClient的onJsAlerta,onJsConfirm,onJsPrompt,不存在漏洞問題,使用復雜,需要進行協議的約束,可以返回值,能滿足大多數情況下的互調通信。
WebView的一些漏洞以及如何防止
webview默認開啟了密碼保存功能,在用戶輸入密碼后會彈出提示框詢問用戶是否保存密碼,保存后密碼會被明文保存在 /data/data/com.package.name/databases/webview.db 下面,手機root后可以查看,那么如何解決?
WebSettings.setSavePassword(false) // 關閉密碼保存提醒功能
addJavascriptInterface漏洞,首先先明白一點,js調用Android代碼的時候,我們經常使用的是addJavascriptInterface, JS調用Android的其中一個方式是通過addJavascriptInterface接口進行對象映射,那么Android4.2之前,既然拿到了這個對象,那么這個對象中的所有方法都是可以調用的,4.2之后,需要被js調用的函數加上@JavascriptInterface注解后來避免該漏洞
所以怎么解決
對于Android 4.2以前,需要采用攔截prompt 方式進行漏洞修復
對于Android 4.2以后,則只需要對被調用的函數以 @JavascriptInterface進行注解
原因分析 當我們在Applilcation里面,android:exported="true"的時候,A 應用可以通過 B 應用導出的 Activity 讓 B 應用加載一個惡意的 file 協議的 url,從而可以獲取 B 應用的內部私有文件,從而帶來數據泄露威脅,
下面來看下WebView中getSettings類的方法對 WebView 安全性的影響 setAllowFileAccess
// 設置是否允許 WebView 使用 File 協議
// 默認設置為true,即允許在 File 域下執行任意 JavaScript 代碼
webView.getSettings.setAllowFileAccess(true);
如果設置為false的話,便不會存在威脅,但是,webview也無法使用本地的html文件
setAllowFileAccessFromFileURLs
// 設置是否允許通過 file url 加載的 Js代碼讀取其他的本地文件
// 在Android 4.1前默認允許
// 在Android 4.1后默認禁止
webView.getSettings.setAllowFileAccessFromFileURLs(true);
我們應該明確的設置為false,禁止讀取其他文件
setAllowUniversalAccessFromFileURLs
// 設置是否允許通過 file url 加載的 Javascript 可以訪問其他的源(包括http、https等源)
// 在Android 4.1前默認允許(setAllowFileAccessFromFileURLs不起作用)
// 在Android 4.1后默認禁止
webView.getSettings.setAllowUniversalAccessFromFileURLs(true);
WebView預加載以及資源預加載
h5頁面加載慢,慢的原因:頁面渲染慢,資源加載慢
h5的緩存,資源預加載,資源攔截
h5的緩存 Android WebView自帶的緩存
1. 瀏覽器緩存
根據 HTTP 協議頭里的 Cache-Control(或 Expires)和 Last-Modified(或Etag)
等字段來控制文件緩存的機制瀏覽器自己實現,我需我們處理
2. App Cache
方便構建Web App的緩存,存儲靜態文件(如JS、CSS、字體文件)
WebSettings settings=getSettings;
String cacheDirPath=context.getFilesDir.getAbsolutePath+"cache/";
settings.setAppCachePath(cacheDirPath);
settings.setAppCacheMaxSize(20*1024*1024);
settings.setAppCacheEnabled(true);
3. Dom Storage
WebSettings settings=getSettings;
settings.setDomStorageEnabled(true);
4. Indexed Database
// 只需設置支持JS就自動打開IndexedDB存儲機制
// Android 在4.4開始加入對 IndexedDB 的支持,只需打開允許 JS 執行的開關就好了。
WebSettings settings=getSettings;
settings.setJavaScriptEnabled(true);
資源預加載 預加載webview對象,首次初始化WebView會比第二次慢很多的原因:初始化后,即使webview已經釋放,但是WebView的一些共享的對象依然是存在的,我們可以在Application里面提前初始化一個Webview的對象,然后可以直接loadurl加載資源
資源攔截 可以將跟新頻率低的一些資源靜態文件放在本地,攔截h5的資源網絡請求并進行檢測,如果檢測到,就直接拿本地的資源進行替換即可
// 攔截資源 通常用于h5的首頁頁面,將常用的一些資源,放到本地
override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
if(request?.url.toString.contains("logo.jpg")){
var inputStream: InputStream?=
inputStream=applicationContext.assets.open("images/test.jpg")
return WebResourceResponse("image/png","utf-8", inputStream)
}
return super.shouldInterceptRequest(view, request)
}
常見的使用注意事項
在manifest Application標簽下面使用:
android:usesCleartextTraffic="true"
#保留annotation, 例如 @JavascriptInterface 等 annotation
-keepattributes *Annotation*
#保留跟 javascript相關的屬性
-keepattributes JavascriptInterface
#保留JavascriptInterface中的方法
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}
#這個類是用來與js交互,所以這個類中的 字段 ,方法, 不能被混淆、全路徑名稱.類名
-keepclassmembers public class com.youpackgename.xxx.H5CallBackAndroid{
<fields>;
<methods>;
public *;
private *;
}
3.1 在WebViewActivity里面,開啟調試
// 開啟調試
WebView.setWebContentsDebuggingEnabled(true)
3.2 chrome瀏覽器地址欄輸入 chrome://inspect
3.3 手機打開USB調試,打開webview頁面,點擊chrome頁面的最下面的inspect,這樣,便可以進入了web開發,看控制臺,網絡請求等
今日問題:
看到WebView是不是很頭疼?
系 大淘寶技術 2024-06-28 17:25 浙江
從零基礎接手DX擴展開發維護,到完成DX擴展從O2平臺遷移到 VSCode 平臺,現在也積累了一些經驗,本文將對這一過程中的學習經歷做一個簡單小結,也希望可以通過本文幫助想要開發 VSCode 擴展的同學可以更快速的上手。
VSCode (Visual Studio Code) 是微軟開發的一款免費、開源的代碼編輯器。它基于 Electron 框架構建,提供了豐富的開發者工具,支持多種編程語言,可以進行代碼調試、版本控制、智能提示等功能,是很多開發者日常使用的工具。
Electron
理解 vscode,我們首先要談的是 Electron。
Electron 的核心技術主要包括以下幾個方面:
Electron 還有一個很大特點就是多進程。主要的有以下兩個進程:
綜上來看:在 Electron 應用中,web 頁面可以通過渲染進程將消息轉發到主進程中,進而調用操作系統的 native api。相比普通 web 應用,可開發擴展的能力更加靈活、豐富。
了解了 vscode 的底層設計,下面我們就以真實的需求(創建模板)來一步步探索 vscode 擴展開發。
需求分析
在 vscode 活動欄提供視圖容器,透出創建模板入口,點擊后打開可視化界面,進行簡單配置后完成模板創建(注冊模板信息到模板平臺并生成對應的模板文件)。
要實現以上功能,需要先提煉出幾個和 vscode 相關功能:
邏輯實現
初始化一個插件項目后,暴露在最外面的文件中包含 activate 和 deactvate 兩個方法,這倆方法屬于 vscode 插件的生命周期,最終會被 export 出去給 vscode 主動調用。而 onXXX 等事件是聲明在插件 package.json 文件中的 Activation Events。聲明這些 Activation Events 后,vscode 就會在適當的時機回調插件中的 activate函數。vscode 之所以這么設計,是為了節省資源開銷,只在必要的時候才激活你的插件。
// package.json
"activationEvents": [
"onCommand:dinamicx.createTemplate",
...
],
"commands": [
{
"command": "dinamicx.createTemplate",
"title": "DX: 創建模板"
},
...
],
"menus": {
"view/title": [
{
"command": "dinamicx.createTemplate",
"group": "navigation@0",
"when": "view==dinamicx.views.main"
}
...
]
}
也可以在插件激活時注冊命令:
import { createTemplate } from './commands/createTemplate';
export function activate(context: vscode.ExtensionContext) {
// 注冊命令
vscode.commands.registerCommand('dinamicx.createTemplate', (info: any)=> {
createTemplate(context, info.path);
})
...
}
上面這段代碼的含義是將dinamicx.createTemplate這個命令和函數綁定,具體的邏輯部分應該在createTemplate這個方法中實現。
如果要創建一個頁面,可以使用 vscode 提供的
api——vscode.window.createWebviewPanel:
export function createTemplate(
context: vscode.ExtensionContext,
dirPath: string,
) {
const panel=vscode.window.createWebviewPanel(
'createTemplate', // viewType
'創建模板頁面', // 視圖標題
vscode.ViewColumn.One, // 顯示在編輯器的哪個部位
// 啟用JS,默認禁用 // webview被隱藏時保持狀態,避免被重置
{ enableScripts: true, retainContextWhenHidden: true },
);
...
const htmlContent=this.getHtmlContent(panel.webview, htmlPath);
panel.webview.html=htmlContent;
panel.reveal();
return panel;
}
具體渲染的頁面可以通過 html 屬性指定,但是 html 屬性接收的參數是字符串!那么我們無法使用 vue/react 進行編碼,只能寫模板字符串了嗎?
當然不是!我們可以先編寫 react 代碼,再打包成 js,套在 index.html 模板中 return 出來,問題就迎刃而解。處理這件事情的就是getHtmlContent:
function getHtmlContent(webview, htmlPath) {
/*
各種資源的絕對路徑
const getHTMLDependencies=()=> (`
<!-- Dependencies -->
<script src="${highlightJs}"></script>
<script src="${reactJs}"></script>
<script src="${reactDomJs}"></script>
<script src="${antdJs}"></script>
`);
*/
const { getHTMLLinks, getHTMLDependencies }=useWebviewBasic(context);
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
${getHTMLLinks()}
</head>
<style>
body {
background-color: transparent !important;
}
</style>
<body>
<div id="root"></div>
${getHTMLDependencies()}
<!-- Main -->
<script src="vscode-resource:${htmlPath}"></script>
#{endOfBody}
</body>
</html>
`;
}
vscode-resource: 出于安全考慮,Webview 默認無法直接訪問本地資源,它在一個孤立的上下文中運行。它只允許通過絕對路徑訪問特定的本地文件。
由上面的代碼可見,針對一個命令/函數,如果涉及到 webview,只關注渲染代碼(即 SPA 的 js 文件),不關心具體頁面實現,所以可以將編寫 UI 相關的邏輯,提煉到 node 主進程之外。
對于 vscode 插件來講,UI 是獨立的,所以我們可以像創建 react 項目一樣來完成頁面部分的代碼。
const Template: React.FC=()=> {
const [loading, setLoading]=useState(false);
...
return (
<Spin spinning={loading} tip={loadingText}>
<div className="template">
...
</div>
</Spin>
);
};
ReactDOM.render(<Template />, document.getElementById('root'));
在打包方面,剛才提到了我們要根據不同命令加載不同的頁面組件,即不同的 js,所以打包的 entry 是多入口的;為了不重復引入公共庫,將 react、antd 等庫 external,選擇通過 cdn 的方式引入。
const config={
mode: env.production ? 'production' : 'development',
entry: {
template: createPageEntry('page-template'),
layout: createPageEntry('page-layout'),
view: createPageEntry('view-idl'),
...
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, '../dist/webview'),
},
...
externals: {
'react': 'root React',
'react-dom': 'root ReactDOM',
'antd': 'antd',
},
};
當我們實現 Webview 后,下一步是拉取數據,然后渲染到本地項目對應的路徑中,可見這一步需要操作系統 api 的支持,我們需要使用 node 進程來做這件事。
那么問題來了,UI 是通過 html 字符串傳給 vscode 進程的,他們之間是如何通信的呢。
開發 vscode 擴展最核心(惡心)的事情就是通信,單向的數據流導致不僅是 webview 和插件 node 進程通信復雜,即使在同一個 react 項目中的兩個不同頁面(webview)也是不能直接進行數據交互的。
流程如圖:
vscode 在通信這里,只為我們提供了最簡單粗糙的通信方法 —— acquirevscodeApi,這個對象里面有且僅有以下幾個可以和插件通信的 API。
插件發送消息:
panel.webview.postMessage; // 支持發送任意被JSON化的數據
WebView 接收消息:
window.addEventListener('message', (event)=> {
const message=event.data;
console.log(message);
});
WebView 給插件發消息:
export const vscode=acquirevscodeApi();
vscode.postMessage('xxx');
插件接收消息:
panel.webview.onDidReceiveMessage(
(message)=> {
console.log('插件收到的消息:', message);
},
undefined,
context.subscriptions
);
通信封裝
基于以上的進程通信方式,如果所有通信邏輯都通過 message 事件監聽,那怎么知道某一處該接收哪些消息,該如何發送一個具有唯一標識的消息?
vscode 本身沒有提供類似的功能,不過可以自己封裝。
流程如圖:
Webview端:
export abstract class App<> {
// private readonly _api: vscodeApi;
// 單向通信
protected sendCommand<TCommand extends IpcCommandType<any>>(
command: TCommand,
params: IpcMessageParams<TCommand>
): void {
const id=nextIpcId();
this.postMessage({ id: id, method: command.method, params: params });
}
// 雙向通信
protected async sendCommandWithCompletion<
TCommand extends IpcCommandType<any>,
TCompletion extends IpcNotificationType<any>
>(
command: TCommand,
params: IpcMessageParams<TCommand>,
completion: TCompletion
): Promise<IpcMessageParams<TCompletion>> {
const id=nextIpcId();
const promise=new Promise<IpcMessageParams<TCompletion>>(
(resolve, reject)=> {
let timeout: ReturnType<typeof setTimeout> | undefined;
const disposables=[
DOM.on(window, 'message', (e: MessageEvent<IpcMessage>)=> {
onIpc(completion, e.data, (params)=> {
if (e.data.completionId===id) {
disposables.forEach((d)=> d.dispose());
queueMicrotask(()=> resolve(params));
}
});
}),
{
dispose: function () {
if (timeout !=null) {
clearTimeout(timeout);
timeout=undefined;
}
},
},
];
timeout=setTimeout(()=> {
timeout=undefined;
disposables.forEach((d)=> d.dispose());
debugger;
reject(
new Error(
`Timed out waiting for completion of ${completion.method}`
)
);
}, 600000);
}
);
this.postMessage({
id: id,
method: command.method,
params: params,
completionId: id,
});
return promise;
}
private postMessage(e: IpcMessage) {
this._api.postMessage(e);
}
}
Node端:
parent.webview.onDidReceiveMessage(this.onMessageReceivedCore, this),
onMessageReceivedCore(e: IpcMessage) {
if (e==null) return;
switch (e.method) {
case ExecuteCommandType.method:
onIpc(ExecuteCommandType, e, params=> {
if (params.args !=null) {
void executeCommand(params.command as Commands, ...params.args);
} else {
void executeCommand(params.command as Commands);
}
});
break;
default:
this.provider.onMessageReceived?.(e);
break;
}
}
// commands.ts
import { commands } from 'vscode';
export function executeCommand<U=any>(command: Commands): Thenable<U>;
export function executeCommand<T=unknown, U=any>(command: Commands, arg: T): Thenable<U>;
export function executeCommand<T extends [...unknown[]]=[], U=any>(command: Commands, ...args: T): Thenable<U>;
export function executeCommand<T extends [...unknown[]]=[], U=any>(command: Commands, ...args: T): Thenable<U> {
return commands.executeCommand<U>(command, ...args);
}
需求實現
基于以上,視圖層、邏輯層、通信層的框架就大致完成了,接下來就是基于需求本身實現視圖(react)和邏輯(node)的實現了。
希望此文能幫助大家快速對 vscode 插件開發有一定了解。后續會再介紹基于 vscode 的 DX 插件和使用建議、以及提高 vscode 開發效率的配置分享~
參考資料
團隊介紹
我們是淘天集團 - 終端體驗平臺團隊,立足于淘寶體驗平臺及集團移動中臺定位,致力于無線端到端前沿技術探索,深入終端廠商到原生系統技術挖掘,打造集團先進且行業領先的終端基礎設施及配套服務,涵蓋多端性能體驗、終端技術服務、原生技術研發、用戶增長觸達等關鍵領域的工作,為阿里巴巴數百款活躍App提供研發與性能支撐,即是集團終端技術生態的基石團隊之一,也是淘天雙11核心支撐團隊之一!
行到水窮處,坐看云起時
原文鏈接
一、概述
1、背景
2、H5頁面的體驗問題
從用戶角度,相比Native頁面,H5頁面的體驗問題主要有兩點:
這里討論的是:第一點,怎樣減少白屏時間。
二、Webview打開H5
通過Webview打開H5頁面,請求并得到 HTML、CSS 和 JavaScript 等資源并對其進行處理從而渲染出 Web 頁面。
1、加載流程
2、H5頁面渲染
對H5頁面的渲染,主要包括:渲染樹構建、布局及繪制,具體可分為:
說明:這五個步驟并不一定一次性順序完成。如果 DOM 或 CSSOM 被修改,以上過程需要重復執行,這樣才能計算出哪些像素需要在屏幕上進行重新渲染。實際頁面中,CSS 與 JavaScript 往往會多次修改 DOM 和 CSSOM。具體參考:DOM渲染機制與常見性能優化
3、總結
降低請求量:合并資源,減少 HTTP 請求數,minify / gzip 壓縮,webP,lazyLoad。 加快請求速度:預解析DNS,減少域名數,并行加載,CDN 分發。 緩存:HTTP 協議緩存請求,離線緩存 manifest,離線數據緩存localStorage。 渲染:JS/CSS優化,加載順序,服務端渲染,pipeline。 復制代碼
二、WebView的客戶端優化(trick版)
由于是接入第三方的H5頁面,接入離線包方案,需要比較繁雜的商務溝通和技術挑戰(業務邏輯和代碼超級詭異),臨時采用如下優化方案。
1、預加載資源
2、預初始化Webview
3、最終方案(迫不得已)
?由于第三方業務H5很多問題,和人力上不足;不得不需要客戶端強行配合優化,在產品的要求下,不得不采用如下方案,方案的前提是:業務H5盡可能少修改,甚至不修改,客戶端還要保證首屏加載快;
4、方案的后遺癥
三、離線包方案
1、概述
2、方案描述
引用bang的離線包方案,簡單描述如下:
說明:目前WKWebView已經能成為主流,但是WKWebView在實現離線包方案時,攔截網絡請求有坑。
3、WKWebView攔截網絡請求的坑
//蘋果開源的 WebKit2 源碼暴露了私有API: + [WKBrowsingContextController registerSchemeForCustomProtocol:] //通過注冊 http(s) scheme 后 WKWebView 將可以使用 NSURLProtocol 攔截 http(s) 請求: Class cls=NSClassFromString(@"WKBrowsingContextController”); SEL sel=NSSelectorFromString(@"registerSchemeForCustomProtocol:"); if ([(id)cls respondsToSelector:sel]) { // 注冊http(s) scheme, 把 http和https請求交給 NSURLProtocol處理 [(id)cls performSelector:sel withObject:@"http"]; [(id)cls performSelector:sel withObject:@"https"]; } 復制代碼
**說明1:**名目張膽使用私有API,是過不了AppStore審核的,具體使用什么辦法,想來你也懂(hun xiao)。
說明2:一旦打開ATS開關:Allow Arbitrary Loads 選項設置為NO,通過 registerSchemeForCustomProtocol 注冊了 http(s) scheme,WKWebView 發起的所有 http(s) 網絡請求將被阻塞(即便將Allow Arbitrary Loads in Web Content 選項設置為YES);
說明3:iOS11之后可以通過WKURLSchemeHandler去完成對WKWebView的請求攔截,不需要再調用私有API解決上述問題了。
4、WKWebView自定義資源scheme
四、其他
1、LocalWebServer
CocoaHttpServer (支持iOS、macOS及多種網絡場景) GCDWebServer (基于iOS,不支持 https 及 webSocket) Telegraph (Swift實現,功能較上面兩類更完善) 復制代碼
2、WKWebView loadRequest 問題
//同樣是由于進程間通信性能問題,HTTPBody字段被丟棄 [request setHTTPMethod:@"POST"]; [request setHTTPBody:[@"bodyData" dataUsingEncoding:NSUTF8StringEncoding]]; [wkwebview loadRequest: request]; 復制代碼
解決:假如想通過-[WKWebView loadRequest:]加載 post 請求 (原始請求)request1: h5.nanhua.com/order/list,可以通過以下步驟實現:
3、推薦資料
*請認真填寫需求信息,我們會在24小時內與您取得聯系。