您好,登錄后才能下訂單哦!
本文小編為大家詳細介紹“Flutter WebView預加載如何實現”,內容詳細,步驟清晰,細節處理妥當,希望這篇“Flutter WebView預加載如何實現”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來學習新知識吧。
WebView是在APP中,可以很方便的展示web頁面,并且與web交互APP的數據。方便,并且更新內容無需APP發布新版本,只需要將最新的web代碼部署完成,用戶重新刷新即可。
在WebView中,經常能夠聽到的一個需求就是:減少首次白屏時間,加快加載速度。因為加載web頁面,必然會受到網絡狀況等的影響,無法像原生內容一樣把靜態內容秒加載出來。
在原生Android和iOS中,有一種預緩存資源,并在加載時攔截web請求,將事先緩存好的資源替換上去,從而實現預加載的方案。
iOS常見的攔截的框架是CocoaHTTPServer / Telegraph
Android則是在WebViewClient中shouldInterceptRequest去進行攔截
道理都是一樣的。
那么,Flutter有沒有類似的方式去實現預加載web資源呢?
有!類似iOS中的CocoaHTTPServer,flutter也有一個HttpServer,可以發現,他們基本是一樣的功能,并且Flutter HttpServer支持Android和iOS。
HttpServer包含在http的包中,在pub.dev找到最新的版本加入即可。
dependencies: flutter: sdk: flutter http: ^0.13.4
權限要求
因為要http服務,所以需要配置一下允許各平臺的http請求。
啟動服務
abstract class HttpServer implements Stream<HttpRequest>
var server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
HttpServer.bind方法會開啟偵聽對應Address的請求,第一個入參address可以自定,第二個port可以為0,也可以自定,為0的話,則由系統隨機分配一個臨時端口。
異步返回一個HttpServer,可以拿到最終的地址,也可以配置一些屬性
curAddresses = _server!.address.address; curPort = _server!.port; _server!.sessionTimeout = 60;
并且,可以設置攔截偵聽!
serverSub = _server!.listen(_responseWebViewReq, onError: (e) => log(e, name: _logKey));
listen即常見的StreamSubscription,關閉時需要Cancel。 在listen的onDate中,會提供一個HttpRequest,即被攔截的請求的HttpRequest。
_responseWebViewReq(HttpRequest request)
我們可以取得其當前請求的Uri,并且可以根據不同的Uri,返回不同的結果給到該請求的response
var uri = request.requestedUri; final data = await _getResponseData(uri); request.response.add(data);
也可以設置headers
request.response.headers.add('Content-Type', '$mime; charset=utf-8');
finally,在所有請求結束時,關閉該response
request.response.close();
至此,HttpServer攔截的功能就實現了。
當然僅僅實現HttpServer攔截是不夠的,既然我們要實現預加載,最主要的攔截方案已經有了,那么,接下來就需要考慮,資源的配置,資源的下載和存儲,版本的管理,如何根據實際url獲取對應HttpServer bind的url等。不在意的話也可以直接跳到最后看Demo。
PS:因為項目中命名為LocalServerWebview,所以后面代碼中可能稱其為LocalServer。
我們需要知道,哪些資源是需要被下載的,被使用在LocalServer服務中的。所以我設計了一個json配置文件,存儲在服務端中,每次打開App時下發。大致的格式為:
{ "option": [ { "key": "test", "open": 1, "priority": 0, "version": "20222022" }, { "key": "test2", "open": 0, "priority": 0, "version": "20222222" } ], "assets": { "test": { "compress": "/local-server/test.zip" }, "test2": { "compress": "/local-server/test2.zip" } }, "basics": { "common": { "compress": "/local-server/common.zip", "version": "20220501" } }, "local_server_open": 1 }
主要根據我這邊的web項目配置,option為配置的對應webPath的開關、下載優先級、版本號,
assets中則是option對應的key的壓縮包地址(也可以一起寫在option中,不過實際業務中還有別的配置,所以就這樣吧)
basics則是統一資源的配置,比如common,所有web通用的js、json資源等,便統一下載,避免重復。
local_server_open是總開關,關閉時則LocalServer服務不會使用。
然后便是獲取到配置后,對符合條件的資源進行下載解壓和存儲。
// 觸發basics預下載 LocalServerDownloadService.instance.preloadBasicsData(json['basics'], basics, oldBasic);
// 觸發assets預下載 LocalServerDownloadService.instance.preloadAssetsData(_diffAssets(value, assets));
這邊使用的Dio進行download,
Dio().download(queueItem.zipUrl, zipPath).then((resp) { if (resp.statusCode != 200) { _log('下載ls 壓縮包失敗 err:${resp.statusCode} zipUrl:${queueItem.zipUrl}'); throw Exception('下載ls 壓縮包失敗 err:${resp.statusCode}'); } return unarchive(queueItem, zipPath); })
archive包進行解壓
// 找到對應zipUrl的本地文件路徑 Directory saveDirct = LocalServerConfiguration.getCurrentZipPathSyncDirectory(item.zipUrl); final zipFile = File(downPath); if (!zipFile.existsSync()) { throw Exception('Local server 下載包文件路徑不存在:$downPath'); } List<int> bytes = zipFile.readAsBytesSync(); Archive archive = ZipDecoder().decodeBytes(bytes); ··· // 清理之前的緩存 File oldfile = File(downPath); if (oldfile.existsSync()) { oldfile.deleteSync(); }
zip文件在解壓完成后會被清理,根據zipUrl來決定存儲的文件路徑。 若已經存在資源,則無需下載。
若是下載失敗的話,會被標記為failure,在重啟app后的新下載任務中會重新嘗試。 也可以加個重試幾次的邏輯。
queueItem.loadState = LoadStateType.failure; queueItem.downloadCount += 1;
在配置json中可以看到version相關的設置,在上一步的下載解壓完成之后,會把文件狀態、對應的option、assets、basics數據(版本)存儲起來。
首先檢查對應的版本號是否能對上,若對不上的話,舊的數據將不會用來去重,而是直接使用最新獲取到的配置進行下載和覆蓋。
// 處理 assets 資源,和版本控制 LocalServerConfigCache.getOptions().then((oldOptions) { // assets 緩存和版本處理 LocalServerConfigCache.getAssets().then((value) { var oldAssets = value; // 版本不對,則移除,并需要下載 if (oldOptions != null) { for (var e in oldOptions) { var res = options.where((element) => element.key == e.key); if (res.isNotEmpty && res.first.version != e.version) { _log('資源 ${e.key} 需要更新'); oldAssets?.removeWhere((key, value) => key == e.key); } } } // 觸發預下載 LocalServerDownloadService.instance.preloadAssetsData(_diffAssets(value, assets)); **});** });
在預下載加入下載隊列前,會檢查之前存儲的文件狀態,若是suceess,則跳過不進行下載。
_assetsBucket.forEach((key, value) { for (var tmpItem in value) { switch(tmpItem.loadState) { case LoadStateType.unLoad: case LoadStateType.loading: _addQueue(tmpItem); break; case LoadStateType.success: sucCount++; break; case LoadStateType.failure: _addQueue(tmpItem); break; } } });
打開Webview前,則需要打開LocalServer服務,并且可以根據不同的url獲取得到對應的LocalServerUrl。
return LocalServerService.instance.getLocalServerWebUrl(h6Path, query.isEmpty ? path : path + '?' + query);
String _getLocalServerWebUrl(String oriUrl, String localServerKey) { return 'http://${curAddresses ?? InternetAddress.loopbackIPv4.address}:$curPort$localServerKey'; }
其實就是在bind成功之后,將address和port存儲下來,并在獲取的時候將query與其拼接。
然后將處理后的url給到webview進行加載,即會觸發
這里有個處理是將basics統一資源的鏈接,動態的添加到每個web頁面的資源列表里。Binder在初始化配置和資源下載完成后,會存儲Config和basicCache到內存中。并且統記webpage打開數量,避免HttpServer還在使用時被關閉。
@override void initState() { super.initState(); log('頁面開始加載:${DateTime.now()}', name: 'web-time'); _localServerBuilder = LocalServerCacheBinder()..initBinder(); LocalServerWebViewManager.instance.registerBuilder(_localServerBuilder); _innerUrl = _localServerBuilder.convertH5Url2LocalServerUrl(widget.url); }
WebView
WebView( initialUrl: _innerUrl, debuggingEnabled: true, ··· )
會存在些情況就是,預加載的資源還沒有下載解壓完成或者說資源下載失敗了,用戶就開啟了Webview,這時候我們就需要用源鏈接(baseDomain)去實時獲取到數據來替換,避免web頁面異常。
// 找不到本地文件,使用網絡下載拿到原始數據 var nowUri = request.requestedUri; var baseDomain = LocalServerCacheBinderSetting.instance.baseDomain; var baseUri = Uri.parse(baseDomain); // 替換為原始url nowUri = nowUri.replace( scheme: baseUri.scheme, host: baseUri.host, port: baseUri.port); // dio請求,responseType 必須是bytes var res = await Dio().getUri(nowUri, options: Options(responseType: ResponseType.bytes)); data = res.data; name = basename(nowUri.path.split('/').toList().last); mime = lookupMimeType(name); request.response.headers.add('Content-Type', '$mime; charset=utf-8'); return data;
最終所有的模塊由一個manager進行統一管理,繼承LocalServerClientManger,設置相應的初始化和配置即可。
class LocalServerClientManager implements LocalServerStatusHandler, LocalServerDownloadServiceProtocol
class LocalServerWebViewManager extends LocalServerClientManager { factory LocalServerWebViewManager() => _getInstance(); static LocalServerWebViewManager get instance => _getInstance(); static LocalServerWebViewManager? _instance; static LocalServerWebViewManager _getInstance() { _instance ??= LocalServerWebViewManager._internal(); return _instance!; } LocalServerWebViewManager._internal(); /// 測試的配置 void initSetting() { init(); LocalServerCacheBinderSetting.instance.setBaseHost('https://jomin-web.web.app'); Map<String, dynamic> baCache = {'common': {'compress': '/local-server/common.zip', "version": "20220503"}}; LocalServerClientConfig localServerClientConfig = LocalServerClientConfig.fromJson({ 'option': [{'key': 'test-one', 'open': 1, 'priority': 0, "version": "20220503"}], 'assets': { 'test-one': {'compress': '/local-server/test-one.zip'} }, 'basics': baCache, }); prepareManager(localServerClientConfig); startLocalServer(); } }
可以寫對應的獲取配置json的方法,設置上去,然后在需要的時候打開LocalServer。
Android模擬機展示
分析
使用我這邊的幾個實際項目中的webview進行測試,對于越“靜態”的頁面的優化效果越好,就是說,可被LocalServer實際服務到的資源越多,首次加載的優化效果就越好。
比如純靜態頁面,iOS的加載完成時間,取20次首次加載的平均值,
未開啟LocalServer的平均加載時間為343ms
開啟LocalServer的平均加載時間為109ms
(時間由Safari的網頁檢查器統計)
非首次則優化相對沒有這么明顯,因為未開啟情況下除了html均會被緩存。
未開啟LocalServer的非首次平均加載時間為142ms
開啟LocalServer的非首次平均加載時間為109.4ms
未開啟的最快的加載時間還會比開啟的快。由html的加載速度決定。
若是非純靜態頁面,開啟和未開啟的時間都會受到網絡狀況的影響,開啟LocalServer依舊有優化效果,
未開啟LocalServer
開啟LocalServer
但可以看到靜態資源的讀取速度LocalServer下依舊比較快,而其他的資源則不穩定了。
對于打包到資源包中的資源,首次加載LocalServer可以有比較明顯的優化效果,且速度比較穩定,不會受到網絡波動的影響。
但是呢,使用了LocalServer,便無法使用瀏覽器自身的緩存,對于非首次情況優化效果不大。
并且,LocalServer可能會有更新的問題,何時去檢查配置是否有更新?或許可以通過長鏈下發通知的方式,但沒有長鏈的話就得考慮下其他的方法來解決更新及時性的問題了。
Demo地址:github.com/EchoPuda/lo…
是個插件形式,可以直接使用。 有些東西可以根據業務調整,比如新增特殊的配置、資源包是否要分包、LocalServer的服務也可以根據url來開啟不同的服務等。
我是觸發預加載后會將下載成功或已經成功的資源保存到內存中,也可以在讀取時再進行對應的IO讀取文件,速度會相應慢一點。
讀到這里,這篇“Flutter WebView預加載如何實現”文章已經介紹完畢,想要掌握這篇文章的知識點還需要大家自己動手實踐使用過才能領會,如果想了解更多相關內容的文章,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。