整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          WebView的坑別嫌多

          WebView的坑別嫌多

          個蛋(codeegg)第 723 次推文

          作者: 騎著蝸牛闖紅燈

          原文: https://juejin.im/post/5cff8c27f265da1bae38f1c1

          先簡單介紹一下,Android在4.4之后采用了Chrome內核,所以我們在開發web頁面的時候,es6的語法,css3的樣式等大可放心使用。

          我將分下面幾個模塊去介紹Android上面WebView。

          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) // 關閉密碼保存提醒功能

          WebView 任意代碼執行漏洞

          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)
          }

          常見的使用注意事項

          1. Android9.0 已經禁止了webview使用http,怎么解決?

          在manifest Application標簽下面使用:

          android:usesCleartextTraffic="true"

          2. 開啟混淆之后,Android無法與h5交互?

          #保留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. 如何調試?

          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 的核心技術主要包括以下幾個方面:

          • Chromium: Electron 使用了 Chromium 瀏覽器作為其渲染引擎。Chromium是 Google Chrome 的開源版本,負責處理和渲染應用程序的用戶界面,包括 HTML、CSS 和 JavaScript。這使得開發者可以利用Web開發技術來構建應用的界面。
          • Node.js: Electron 集成了 Node.js,使得開發者可以在應用程序的主進程(后臺)中運行 JavaScript 代碼。Node.js 提供了對文件系統、網絡、進程等系統級 API 的訪問,增強了應用程序的功能和交互性。
          • Native API: Electron 提供了一套 API,允許主進程和渲染進程之間進行通信,以及調用操作系統級別的功能。這些 API 包括 ipcRenderer 和 ipcMain(用于進程間通信)、webContents(用于控制頁面內容)等。



          Electron 還有一個很大特點就是多進程。主要的有以下兩個進程:

          • 主進程
            • Electron 中運行 package.json 中的 main 腳本的進程被稱為主進程,即 main.js 就是運行在主進程。
            • 一個 electron 應用有且只有一個主進程。
            • 只有主進程可以直接進行 GUI 相關的原生 API 操作。


          • 渲染進程
            • 運行在 Chromium 的 web 頁面姑且叫渲染進程,即運行 index.html 的環境就是渲染進程。
            • 一個 electron 應用可以有多個渲染進程。
            • 渲染進程在引入 Node.js 模塊的前提下,可以在頁面中和操作系統進行一些底層交互(如 fs 模塊)。


          綜上來看:在 Electron 應用中,web 頁面可以通過渲染進程將消息轉發到主進程中,進而調用操作系統的 native api。相比普通 web 應用,可開發擴展的能力更加靈活、豐富。


          了解了 vscode 的底層設計,下面我們就以真實的需求(創建模板)來一步步探索 vscode 擴展開發。



          需求分析


          在 vscode 活動欄提供視圖容器,透出創建模板入口,點擊后打開可視化界面,進行簡單配置后完成模板創建(注冊模板信息到模板平臺并生成對應的模板文件)。


          要實現以上功能,需要先提煉出幾個和 vscode 相關功能:

          • 通過 vscode 指令系統,注冊一個命令到菜單欄。
          • 創建一個用于配置的 web 頁面。
          • 完成配置后上傳配置信息并創建文件。
          • 完成配置后關閉 web 頁面。


          邏輯實現


          ?注冊指令



          初始化一個插件項目后,暴露在最外面的文件中包含 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這個方法中實現。


          ?創建WebView


          如果要創建一個頁面,可以使用 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 主進程之外。


          ?React 和 Webpack


          對于 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 開發效率的配置分享~


          參考資料


          • Introduction | Electron (electronjs.org):
          • https://www.electronjs.org/docs/latest/?spm=ata.21736010.0.0.317e4797PUtlD0
          • Webview API | Visual Studio Code Extension API:
          • https://code.visualstudio.com/api/extension-guides/webview?spm=ata.21736010.0.0.317e4797PUtlD0


          團隊介紹


          我們是淘天集團 - 終端體驗平臺團隊,立足于淘寶體驗平臺及集團移動中臺定位,致力于無線端到端前沿技術探索,深入終端廠商到原生系統技術挖掘,打造集團先進且行業領先的終端基礎設施及配套服務,涵蓋多端性能體驗、終端技術服務、原生技術研發、用戶增長觸達等關鍵領域的工作,為阿里巴巴數百款活躍App提供研發與性能支撐,即是集團終端技術生態的基石團隊之一,也是淘天雙11核心支撐團隊之一!

          ebview加載H5優化小記

          行到水窮處,坐看云起時

          原文鏈接

          一、概述

          1、背景

          • 鑒于H5的優勢,客戶端的很多業務都由H5來實現,Webview成了App中H5業務的唯一載體。
          • WebView組件是iOS組件體系中非常重要的一個,之前的UIWebView 存在嚴重的性能和內存消耗問題,iOS 8之后推出WKWebView,旨在代替UIWebView;
          • WKWebView在性能、穩定性、內存占用上有很大的提升,支持更多的HTML5特性,高達60fps的滾動刷新率以及內置手勢;可以通過KVO監控網絡加載的進度,獲取網頁title;
          • 實踐中,大部分App的H5業務將由WKWebview承載。

          2、H5頁面的體驗問題

          從用戶角度,相比Native頁面,H5頁面的體驗問題主要有兩點:

          • 頁面打開時間慢:打開一個 H5 頁面需要做一系列處理,會有一段白屏時間,體驗糟糕。
          • 響應流暢度較差:由于 WebKit 的渲染機制,單線程,歷史包袱等原因,頁面刷新/交互的性能體驗不如原生。

          這里討論的是:第一點,怎樣減少白屏時間。

          二、Webview打開H5

          通過Webview打開H5頁面,請求并得到 HTML、CSS 和 JavaScript 等資源并對其進行處理從而渲染出 Web 頁面。

          1、加載流程

          • 初始化Webview -> 請求頁面 -> 下載數據 -> 解析HTML -> 請求 js/css 資源 ->DOM 渲染 -> 解析 JS 執行 -> JS 請求數據 -> 解析渲染 -> 下載渲染圖片-> 頁面完整展示

          • DOM渲染之前耗時主要在兩部分:初始化Webview數據請求,一般Webview首次初始化在400ms這個量級,二次加載能少一個量級。
          • 數據請求依賴網絡,網絡請求一般經過:DNS查詢、TCP 連接、HTTP 請求和響應。數據包括HTML、JS和CSS資源,這些都是在webview在loadRequest:之后做的,這一階段,用戶所見到的都是白屏。(雖然4G已經成為主流,但是4G延遲明顯高于Wifi)。

          2、H5頁面渲染

          對H5頁面的渲染,主要包括:渲染樹構建、布局及繪制,具體可分為:

          • 處理 HTML 標記并構建 DOM 樹。
          • 處理 CSS 標記并構建 CSSOM(CSS Object Model) 樹。
          • 將 DOM 與 CSSOM 合并成一個渲染樹。
          • 根據渲染樹來布局,以計算每個節點的幾何信息。
          • 將各個節點繪制到屏幕上。

          說明:這五個步驟并不一定一次性順序完成。如果 DOM 或 CSSOM 被修改,以上過程需要重復執行,這樣才能計算出哪些像素需要在屏幕上進行重新渲染。實際頁面中,CSS 與 JavaScript 往往會多次修改 DOM 和 CSSOM。具體參考:DOM渲染機制與常見性能優化

          3、總結

          • 分析Webview打開H5打開的過程,我們發現,在H5優化中,前端重任在肩;
          降低請求量:合并資源,減少 HTTP 請求數,minify / gzip 壓縮,webP,lazyLoad。
          加快請求速度:預解析DNS,減少域名數,并行加載,CDN 分發。
          緩存:HTTP 協議緩存請求,離線緩存 manifest,離線數據緩存localStorage。
          渲染:JS/CSS優化,加載順序,服務端渲染,pipeline。
          復制代碼
          
          • 但是客戶端也很重要,主要優化DOM渲染之前這些事情,可以做有:減少DNS時間預初始化WebView 以及 HTML、JS、CSS等資源離線下載
          • 列舉在某業務中筆者實踐過的比較trick的優化方案,然后再引出筆者認為理想的方案。

          二、WebView的客戶端優化(trick版)

          由于是接入第三方的H5頁面,接入離線包方案,需要比較繁雜的商務溝通和技術挑戰(業務邏輯和代碼超級詭異),臨時采用如下優化方案

          1、預加載資源

          • 將首頁面需要的JS文件和CSS文件等資源放在一個URL地址(和業務url同域名);
          • 啟動App后,間隔X秒去加載;加載的策略是,檢查當前和上一次間隔時間,超時則加載,有效期忽略預加載請求。

          2、預初始化Webview

          • 首次初始化Webview,需要初始化瀏覽器內核,需要的時間在400ms這個量級;二次初始化時間在幾十ms這個量級;
          • 根據此特征:選擇在APP 啟動后X秒,預創建(初始化)一個 Webview 然后釋放,這樣等使用到 H5 模塊,再加載 Webview時,加載時間也少了不少。
          • 結合步驟一中預加載公共資源,也需要Webview,所以選擇在加載公共資源包時候,首次初始化Webview,加載資源,然后釋放。

          3、最終方案(迫不得已)

          ?由于第三方業務H5很多問題,和人力上不足;不得不需要客戶端強行配合優化,在產品的要求下,不得不采用如下方案,方案的前提是:業務H5盡可能少修改,甚至不修改,客戶端還要保證首屏加載快;

          • 預加載資源
          • 預創建Webview并加載首頁H5,駐留在內存中,需要的時候,立刻顯示。

          4、方案的后遺癥

          • 我不建議這種trick做法,因為自從開了這個口子,后續很多H5需求不走之前既定的離線包方案,在內存中預創建多個Webview (最多4個),加載H5時候不用新建Webview,從Webview池中獲取;
          • 此種Webview池方案帶來諸多隱患:內存壓力、詭異的白屏、JS造成的內存泄露,頁面的清空等等問題(填坑填到掉頭發)。

          三、離線包方案

          1、概述

          • 離線包方案才是業務主流的H5加載優化方案,非常建議在客戶端團隊和前端團隊推廣,類似預創建Webview加載H5不應該成為主流。
          • 將每個獨立的H5功能模塊,相關HTML、Javascript、CSS 等頁面內靜態資源打包到一個壓縮包內,客戶端可以下載該離線包到本地,然后打開Webview,直接從本地加載離線包,從而最大程度地擺脫網絡環境對 H5 頁面的影響。
          • 離線包可以提升用戶體驗(頁面加載更快),還可以實現動態更新(在推出新版本或是緊急發布的時候,可以把修改的資源放入離線包,通過更新配置讓應用自動下載更新)

          2、方案描述

          引用bang的離線包方案,簡單描述如下

          • 后端使用構建工具把同一個業務模塊相關的頁面和資源打包成一個文件,同時對文件加密/簽名。
          • 客戶端根據配置表,在自定義時機去把離線包拉下來,做解壓/解密/校驗等工作。
          • 根據配置表,打開某個業務時轉接到打開離線包的入口頁面。
          • 攔截網絡請求,對于離線包已經有的文件,直接讀取離線包數據返回,否則走 HTTP 協議緩存邏輯。
          • 離線包更新時,根據版本號后臺下發兩個版本間的 diff 數據,客戶端合并,增量更新。

          說明:目前WKWebView已經能成為主流,但是WKWebView在實現離線包方案時,攔截網絡請求有坑。

          3、WKWebView攔截網絡請求的坑

          • 雖然NSURLProtocol可以攔截監聽每一個URL Loading System中發出request請求,記住是URL Loading System中那些類發出的請求,也支持AFNetwoking,UIWebView發出的request,NSURLProtocol都可以攔截和監聽。
          • 因為WKWebView 在獨立進程里執行網絡請求。一旦注冊 http(s) scheme 后,網絡請求將從 Network Process 發送到 App Process,這樣 NSURLProtocol 才能攔截網絡請求。
          • 但是在 WebKit2 的設計里使用 MessageQueue 進行進程之間的通信,Network Process 會將請求 encode 成一個 Message,然后通過 IPC(進程間通信) 發送給 App Process。出于性能的原因,encode 的時候 將HTTPBody 和 HTTPBodyStream 這兩個字段丟棄掉()
          • 因此,如果通過 registerSchemeForCustomProtocol 注冊了 http(s) scheme, 那么由 WKWebView 發起的所有 http(s)請求都會通過 IPC 傳給主進程 NSURLProtocol 處理,導致 post 請求 body 被清空
          //蘋果開源的 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

          • 向WKWebView 注冊 customScheme, 比如 dynamic://, 而不是https或http,避免對https或http請求的影響
          • 保證使用離線包功能的請求,沒有post方式,遇到customScheme請求,比如dynamic://www.dynamicalbumlocalimage.com/,通過 NSURLProtocol 攔截這個請求并加載離線數據。
          • iOS 11上, WebKit 提供的WKURLSchemeHandler可實現攔截,需要注意的只允許開發者攔截自定義 Scheme 的請求,不允許攔截 “http”、“https”、“ftp”、“file” 等的請求,否則會crash。

          四、其他

          1、LocalWebServer

          • 離線包方案中,除了攔截請求加載資源的方式,還有種在項目中搭建local web server,用以獲得本地資源。市面有比較完善的框架
          CocoaHttpServer (支持iOS、macOS及多種網絡場景)
          GCDWebServer (基于iOS,不支持 https 及 webSocket)
          Telegraph (Swift實現,功能較上面兩類更完善)
          復制代碼
          
          • 具體可參考 基于 LocalWebServer 實現 WKWebView 離線資源加載, 之前團隊有過實踐,采用的是GCDWebServer

          2、WKWebView loadRequest 問題

          • 在 WKWebView 上通過 loadRequest 發起的 post 請求 body 數據會丟失:
          //同樣是由于進程間通信性能問題,HTTPBody字段被丟棄
          [request setHTTPMethod:@"POST"];
          [request setHTTPBody:[@"bodyData" dataUsingEncoding:NSUTF8StringEncoding]];
          [wkwebview loadRequest: request];
          復制代碼
          

          解決:假如想通過-[WKWebView loadRequest:]加載 post 請求 (原始請求)request1: h5.nanhua.com/order/list,可以通過以下步驟實現:

          • 替換請求 scheme,生成新的 post 請求 request2: post://h5.nanhua.com/order/list, 同時將 request1 的 body 字段復制到 request2 的 header 中(WebKit 不會丟棄 header 字段);
          • 通過-[WKWebView loadRequest:] 加載新的 post 請求 request2;
          • 并且通過 +[WKBrowsingContextController registerSchemeForCustomProtocol:]注冊 scheme: post://;
          • 注冊 NSURLProtocol 攔截請求 post://h5.nanhua.com/order/list ,替換請求 scheme, 生成新的請求 request3: h5.nanhua.com/order/list,將 request2 header的body 字段復制到 request3 的 body 中,并使用 NSURLSession 加載 request3,最后將加載結果返回 WKWebView;

          3、推薦資料

          • 移動端本地 H5 秒開方案探索與實現
          • 使用 PageSpeed Insights 進行移動版分析
          • WebView性能、體驗分析與優化
          • iOS app秒開H5優化總結
          • 賦予H5以Native的生命 ——《WebView優化》

          主站蜘蛛池模板: tom影院亚洲国产一区二区| 无码人妻aⅴ一区二区三区| 国产成人亚洲综合一区| 国产精品无码一区二区在线观| 日韩精品福利视频一区二区三区| 日本精品一区二区三区在线视频| 亚洲一区二区三区在线视频| 香蕉久久一区二区不卡无毒影院| 中文字幕av人妻少妇一区二区| 国产精品综合一区二区三区| 久久精品一区二区影院| 99久久精品日本一区二区免费| 国产视频福利一区| 国产一区二区在线看| 丰满爆乳无码一区二区三区| 国产精品亚洲不卡一区二区三区| 91久久精品国产免费一区| 国产精品视频无圣光一区| 亚洲AV成人一区二区三区AV| 色国产在线视频一区| 无码人妻AⅤ一区二区三区 | 丝袜无码一区二区三区| AV鲁丝一区鲁丝二区鲁丝三区 | 亚洲大尺度无码无码专线一区| 合区精品久久久中文字幕一区| 亚洲一区精彩视频| 国产一区二区不卡在线播放| 中文字幕人妻第一区| 国产一区二区三区久久| 日韩在线观看一区二区三区| 日韩人妻无码一区二区三区99| 亚洲AV无码一区二区三区国产| 国产自产对白一区| 国产视频一区二区在线播放| 又紧又大又爽精品一区二区 | 国产美女视频一区| 国产品无码一区二区三区在线| 国产成人AV一区二区三区无码 | 亚洲电影国产一区| 亚洲一区二区高清| 国产成人精品一区二区三在线观看|