Flutter?WebView?預(yù)加載實(shí)現(xiàn)方法(Http?Server)
背景
WebView是在APP中,可以很方便的展示web頁面,并且與web交互APP的數(shù)據(jù)。方便,并且更新內(nèi)容無需APP發(fā)布新版本,只需要將最新的web代碼部署完成,用戶重新刷新即可。
在WebView中,經(jīng)常能夠聽到的一個(gè)需求就是:減少首次白屏?xí)r間,加快加載速度。因?yàn)榧虞dweb頁面,必然會(huì)受到網(wǎng)絡(luò)狀況等的影響,無法像原生內(nèi)容一樣把靜態(tài)內(nèi)容秒加載出來。
分析
在原生Android和iOS中,有一種預(yù)緩存資源,并在加載時(shí)攔截web請(qǐng)求,將事先緩存好的資源替換上去,從而實(shí)現(xiàn)預(yù)加載的方案。
- iOS常見的攔截的框架是CocoaHTTPServer / Telegraph
- Android則是在WebViewClient中shouldInterceptRequest去進(jìn)行攔截
道理都是一樣的。
那么,F(xiàn)lutter有沒有類似的方式去實(shí)現(xiàn)預(yù)加載web資源呢?
有!類似iOS中的CocoaHTTPServer,flutter也有一個(gè)HttpServer,可以發(fā)現(xiàn),他們基本是一樣的功能,并且Flutter HttpServer支持Android和iOS。
HttpServer
HttpServer包含在http的包中,在pub.dev找到最新的版本加入即可。
dependencies: flutter: sdk: flutter http: ^0.13.4
權(quán)限要求
因?yàn)橐猦ttp服務(wù),所以需要配置一下允許各平臺(tái)的http請(qǐng)求。
啟動(dòng)服務(wù)
abstract class HttpServer implements Stream<HttpRequest>
var server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
HttpServer.bind方法會(huì)開啟偵聽對(duì)應(yīng)Address的請(qǐng)求,第一個(gè)入?yún)ddress可以自定,第二個(gè)port可以為0,也可以自定,為0的話,則由系統(tǒng)隨機(jī)分配一個(gè)臨時(shí)端口。
異步返回一個(gè)HttpServer,可以拿到最終的地址,也可以配置一些屬性
curAddresses = _server!.address.address; curPort = _server!.port; _server!.sessionTimeout = 60;
并且,可以設(shè)置攔截偵聽!
serverSub = _server!.listen(_responseWebViewReq, onError: (e) => log(e, name: _logKey));
listen即常見的StreamSubscription,關(guān)閉時(shí)需要Cancel。 在listen的onDate中,會(huì)提供一個(gè)HttpRequest,即被攔截的請(qǐng)求的HttpRequest。
_responseWebViewReq(HttpRequest request)
我們可以取得其當(dāng)前請(qǐng)求的Uri,并且可以根據(jù)不同的Uri,返回不同的結(jié)果給到該請(qǐng)求的response
var uri = request.requestedUri; final data = await _getResponseData(uri); request.response.add(data);
也可以設(shè)置headers
request.response.headers.add('Content-Type', '$mime; charset=utf-8');
finally,在所有請(qǐng)求結(jié)束時(shí),關(guān)閉該response
request.response.close();
至此,HttpServer攔截的功能就實(shí)現(xiàn)了。
接下來?
當(dāng)然僅僅實(shí)現(xiàn)HttpServer攔截是不夠的,既然我們要實(shí)現(xiàn)預(yù)加載,最主要的攔截方案已經(jīng)有了,那么,接下來就需要考慮,資源的配置,資源的下載和存儲(chǔ),版本的管理,如何根據(jù)實(shí)際url獲取對(duì)應(yīng)HttpServer bind的url等。不在意的話也可以直接跳到最后看Demo。
PS:因?yàn)轫?xiàng)目中命名為L(zhǎng)ocalServerWebview,所以后面代碼中可能稱其為L(zhǎng)ocalServer。
資源配置
我們需要知道,哪些資源是需要被下載的,被使用在LocalServer服務(wù)中的。所以我設(shè)計(jì)了一個(gè)json配置文件,存儲(chǔ)在服務(wù)端中,每次打開App時(shí)下發(fā)。大致的格式為:
{ "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 }
主要根據(jù)我這邊的web項(xiàng)目配置,option為配置的對(duì)應(yīng)webPath的開關(guān)、下載優(yōu)先級(jí)、版本號(hào),
assets中則是option對(duì)應(yīng)的key的壓縮包地址(也可以一起寫在option中,不過實(shí)際業(yè)務(wù)中還有別的配置,所以就這樣吧)
basics則是統(tǒng)一資源的配置,比如common,所有web通用的js、json資源等,便統(tǒng)一下載,避免重復(fù)。
local_server_open是總開關(guān),關(guān)閉時(shí)則LocalServer服務(wù)不會(huì)使用。
然后便是獲取到配置后,對(duì)符合條件的資源進(jìn)行下載解壓和存儲(chǔ)。
// 觸發(fā)basics預(yù)下載 LocalServerDownloadService.instance.preloadBasicsData(json['basics'], basics, oldBasic);
// 觸發(fā)assets預(yù)下載 LocalServerDownloadService.instance.preloadAssetsData(_diffAssets(value, assets));
下載解壓與本地存儲(chǔ)
這邊使用的Dio進(jìn)行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包進(jìn)行解壓
// 找到對(duì)應(yīng)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文件在解壓完成后會(huì)被清理,根據(jù)zipUrl來決定存儲(chǔ)的文件路徑。 若已經(jīng)存在資源,則無需下載。
若是下載失敗的話,會(huì)被標(biāo)記為failure,在重啟app后的新下載任務(wù)中會(huì)重新嘗試。 也可以加個(gè)重試幾次的邏輯。
queueItem.loadState = LoadStateType.failure; queueItem.downloadCount += 1;
版本管理與更新
在配置json中可以看到version相關(guān)的設(shè)置,在上一步的下載解壓完成之后,會(huì)把文件狀態(tài)、對(duì)應(yīng)的option、assets、basics數(shù)據(jù)(版本)存儲(chǔ)起來。
首先檢查對(duì)應(yīng)的版本號(hào)是否能對(duì)上,若對(duì)不上的話,舊的數(shù)據(jù)將不會(huì)用來去重,而是直接使用最新獲取到的配置進(jìn)行下載和覆蓋。
// 處理 assets 資源,和版本控制 LocalServerConfigCache.getOptions().then((oldOptions) { // assets 緩存和版本處理 LocalServerConfigCache.getAssets().then((value) { var oldAssets = value; // 版本不對(duì),則移除,并需要下載 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); } } } // 觸發(fā)預(yù)下載 LocalServerDownloadService.instance.preloadAssetsData(_diffAssets(value, assets)); **});** });
在預(yù)下載加入下載隊(duì)列前,會(huì)檢查之前存儲(chǔ)的文件狀態(tài),若是suceess,則跳過不進(jìn)行下載。
_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; } } });
獲取LocalServer Url并加載Webview
打開Webview前,則需要打開LocalServer服務(wù),并且可以根據(jù)不同的url獲取得到對(duì)應(yīng)的LocalServerUrl。
return LocalServerService.instance.getLocalServerWebUrl(h5Path, query.isEmpty ? path : path + '?' + query);
String _getLocalServerWebUrl(String oriUrl, String localServerKey) { return 'http://${curAddresses ?? InternetAddress.loopbackIPv4.address}:$curPort$localServerKey'; }
其實(shí)就是在bind成功之后,將address和port存儲(chǔ)下來,并在獲取的時(shí)候?qū)?strong>query與其拼接。
然后將處理后的url給到webview進(jìn)行加載,即會(huì)觸發(fā)
這里有個(gè)處理是將basics統(tǒng)一資源的鏈接,動(dòng)態(tài)的添加到每個(gè)web頁面的資源列表里。Binder在初始化配置和資源下載完成后,會(huì)存儲(chǔ)Config和basicCache到內(nèi)存中。并且統(tǒng)記webpage打開數(shù)量,避免HttpServer還在使用時(shí)被關(guān)閉。
@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, ··· )
兜底措施
會(huì)存在些情況就是,預(yù)加載的資源還沒有下載解壓完成或者說資源下載失敗了,用戶就開啟了Webview,這時(shí)候我們就需要用源鏈接(baseDomain)去實(shí)時(shí)獲取到數(shù)據(jù)來替換,避免web頁面異常。
// 找不到本地文件,使用網(wǎng)絡(luò)下載拿到原始數(shù)據(jù) 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請(qǐng)求,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;
統(tǒng)一管理
最終所有的模塊由一個(gè)manager進(jìn)行統(tǒng)一管理,繼承LocalServerClientManger,設(shè)置相應(yīng)的初始化和配置即可。
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(); /// 測(cè)試的配置 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(); } }
可以寫對(duì)應(yīng)的獲取配置json的方法,設(shè)置上去,然后在需要的時(shí)候打開LocalServer。
展示與分析
Android模擬機(jī)展示
分析
使用我這邊的幾個(gè)實(shí)際項(xiàng)目中的webview進(jìn)行測(cè)試,對(duì)于越“靜態(tài)”的頁面的優(yōu)化效果越好,就是說,可被LocalServer實(shí)際服務(wù)到的資源越多,首次加載的優(yōu)化效果就越好。
比如純靜態(tài)頁面,iOS的加載完成時(shí)間,取20次首次加載的平均值,
- 未開啟LocalServer的平均加載時(shí)間為343ms
- 開啟LocalServer的平均加載時(shí)間為109ms
(時(shí)間由Safari的網(wǎng)頁檢查器統(tǒng)計(jì))
非首次則優(yōu)化相對(duì)沒有這么明顯,因?yàn)槲撮_啟情況下除了html均會(huì)被緩存。
- 未開啟LocalServer的非首次平均加載時(shí)間為142ms
- 開啟LocalServer的非首次平均加載時(shí)間為109.4ms
未開啟的最快的加載時(shí)間還會(huì)比開啟的快。由html的加載速度決定。
若是非純靜態(tài)頁面,開啟和未開啟的時(shí)間都會(huì)受到網(wǎng)絡(luò)狀況的影響,開啟LocalServer依舊有優(yōu)化效果,
未開啟LocalServer
開啟LocalServer
但可以看到靜態(tài)資源的讀取速度LocalServer下依舊比較快,而其他的資源則不穩(wěn)定了。
總結(jié)
對(duì)于打包到資源包中的資源,首次加載LocalServer可以有比較明顯的優(yōu)化效果,且速度比較穩(wěn)定,不會(huì)受到網(wǎng)絡(luò)波動(dòng)的影響。
但是呢,使用了LocalServer,便無法使用瀏覽器自身的緩存,對(duì)于非首次情況優(yōu)化效果不大。
并且,LocalServer可能會(huì)有更新的問題,何時(shí)去檢查配置是否有更新?或許可以通過長(zhǎng)鏈下發(fā)通知的方式,但沒有長(zhǎng)鏈的話就得考慮下其他的方法來解決更新及時(shí)性的問題了。
Demo
Demo地址:github.com/EchoPuda/lo…
是個(gè)插件形式,可以直接使用。 有些東西可以根據(jù)業(yè)務(wù)調(diào)整,比如新增特殊的配置、資源包是否要分包、LocalServer的服務(wù)也可以根據(jù)url來開啟不同的服務(wù)等。
我是觸發(fā)預(yù)加載后會(huì)將下載成功或已經(jīng)成功的資源保存到內(nèi)存中,也可以在讀取時(shí)再進(jìn)行對(duì)應(yīng)的IO讀取文件,速度會(huì)相應(yīng)慢一點(diǎn)。
到此這篇關(guān)于Flutter WebView 預(yù)加載實(shí)現(xiàn) Http Server的方法的文章就介紹到這了,更多相關(guān)Flutter WebView 預(yù)加載內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
簡(jiǎn)單談?wù)凙ndroid中SP與DP的區(qū)別
Android里面的sp和dp網(wǎng)上有很多文章都談過了,但是看后總有一種意猶未盡的感覺?,F(xiàn)在我也來談?wù)刣p和sp,和大家交流一下,不對(duì)之處歡迎拍磚。2016-09-09android實(shí)現(xiàn)圖片閃爍動(dòng)畫效果的兩種實(shí)現(xiàn)方式(實(shí)用性高)
本文通過兩種方法給大家講解了android實(shí)現(xiàn)圖片閃爍動(dòng)畫效果,實(shí)用性非常高,對(duì)這兩種方法感興趣的朋友一起通過本文學(xué)習(xí)吧2016-09-09Android仿一號(hào)店貨物詳情輪播圖動(dòng)畫效果
這篇文章主要為大家詳細(xì)介紹了Android-仿一號(hào)店貨物詳情輪播圖動(dòng)畫效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06Android UI設(shè)計(jì)系列之自定義ViewGroup打造通用的關(guān)閉鍵盤小控件ImeObserverLayout(9)
這篇文章主要介紹了Android UI設(shè)計(jì)系列之自定義ViewGroup打造通用的關(guān)閉鍵盤小控件ImeObserverLayout,具有一定的實(shí)用性和參考價(jià)值,感興趣的小伙伴們可以參考一下2016-06-06聊聊GridView實(shí)現(xiàn)拖拽排序及數(shù)據(jù)交互的問題
這篇文章主要介紹了聊聊GridView實(shí)現(xiàn)拖拽排序及數(shù)據(jù)交互的問題,整體實(shí)現(xiàn)思路是通過在一個(gè)容器里放置兩個(gè)dragview,DragView里面進(jìn)行View的動(dòng)態(tài)交換以及數(shù)據(jù)交換,具體實(shí)現(xiàn)代碼跟隨小編一起看看吧2021-11-11Android Tween動(dòng)畫之RotateAnimation實(shí)現(xiàn)圖片不停旋轉(zhuǎn)效果實(shí)例介紹
Android中如何使用rotate實(shí)現(xiàn)圖片不停旋轉(zhuǎn)的效果,下面與大家共同分析下Tween動(dòng)畫的rotate實(shí)現(xiàn)旋轉(zhuǎn)效果,感興趣的朋友可以參考下哈2013-05-05利用Kotlin實(shí)現(xiàn)破解Android版的微信小游戲--跳一跳
這篇文章主要給大家介紹了關(guān)于利用Kotlin實(shí)現(xiàn)破解Android版微信小游戲--跳一跳的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-12-12Android 自定義view模板并實(shí)現(xiàn)點(diǎn)擊事件的回調(diào)
這篇文章主要介紹了Android 自定義view模板并實(shí)現(xiàn)點(diǎn)擊事件的回調(diào)的相關(guān)資料,需要的朋友可以參考下2017-01-01