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頁面,必然會受到網(wǎng)絡(luò)狀況等的影響,無法像原生內(nèi)容一樣把靜態(tài)內(nèi)容秒加載出來。
分析
在原生Android和iOS中,有一種預(yù)緩存資源,并在加載時(shí)攔截web請求,將事先緩存好的資源替換上去,從而實(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ù),所以需要配置一下允許各平臺的http請求。
啟動服務(wù)
abstract class HttpServer implements Stream<HttpRequest>
var server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
HttpServer.bind方法會開啟偵聽對應(yīng)Address的請求,第一個(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中,會提供一個(gè)HttpRequest,即被攔截的請求的HttpRequest。
_responseWebViewReq(HttpRequest request)
我們可以取得其當(dāng)前請求的Uri,并且可以根據(jù)不同的Uri,返回不同的結(jié)果給到該請求的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,在所有請求結(jié)束時(shí),關(guān)閉該response
request.response.close();
至此,HttpServer攔截的功能就實(shí)現(xiàn)了。
接下來?
當(dāng)然僅僅實(shí)現(xiàn)HttpServer攔截是不夠的,既然我們要實(shí)現(xiàn)預(yù)加載,最主要的攔截方案已經(jīng)有了,那么,接下來就需要考慮,資源的配置,資源的下載和存儲,版本的管理,如何根據(jù)實(shí)際url獲取對應(yīng)HttpServer bind的url等。不在意的話也可以直接跳到最后看Demo。
PS:因?yàn)轫?xiàng)目中命名為LocalServerWebview,所以后面代碼中可能稱其為LocalServer。
資源配置
我們需要知道,哪些資源是需要被下載的,被使用在LocalServer服務(wù)中的。所以我設(shè)計(jì)了一個(gè)json配置文件,存儲在服務(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為配置的對應(yīng)webPath的開關(guān)、下載優(yōu)先級、版本號,
assets中則是option對應(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ù)不會使用。
然后便是獲取到配置后,對符合條件的資源進(jìn)行下載解壓和存儲。
// 觸發(fā)basics預(yù)下載 LocalServerDownloadService.instance.preloadBasicsData(json['basics'], basics, oldBasic);
// 觸發(fā)assets預(yù)下載 LocalServerDownloadService.instance.preloadAssetsData(_diffAssets(value, assets));
下載解壓與本地存儲
這邊使用的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)行解壓
// 找到對應(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文件在解壓完成后會被清理,根據(jù)zipUrl來決定存儲的文件路徑。 若已經(jīng)存在資源,則無需下載。
若是下載失敗的話,會被標(biāo)記為failure,在重啟app后的新下載任務(wù)中會重新嘗試。 也可以加個(gè)重試幾次的邏輯。
queueItem.loadState = LoadStateType.failure; queueItem.downloadCount += 1;
版本管理與更新
在配置json中可以看到version相關(guān)的設(shè)置,在上一步的下載解壓完成之后,會把文件狀態(tài)、對應(yīng)的option、assets、basics數(shù)據(jù)(版本)存儲起來。
首先檢查對應(yīng)的版本號是否能對上,若對不上的話,舊的數(shù)據(jù)將不會用來去重,而是直接使用最新獲取到的配置進(jìn)行下載和覆蓋。
// 處理 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);
}
}
}
// 觸發(fā)預(yù)下載
LocalServerDownloadService.instance.preloadAssetsData(_diffAssets(value, assets));
**});**
});在預(yù)下載加入下載隊(duì)列前,會檢查之前存儲的文件狀態(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獲取得到對應(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存儲下來,并在獲取的時(shí)候?qū)?strong>query與其拼接。
然后將處理后的url給到webview進(jìn)行加載,即會觸發(fā)
這里有個(gè)處理是將basics統(tǒng)一資源的鏈接,動態(tài)的添加到每個(gè)web頁面的資源列表里。Binder在初始化配置和資源下載完成后,會存儲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, ··· )
兜底措施
會存在些情況就是,預(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請求,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,
LocalServerDownloadServiceProtocolclass 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();
}
}可以寫對應(yīng)的獲取配置json的方法,設(shè)置上去,然后在需要的時(shí)候打開LocalServer。
展示與分析

Android模擬機(jī)展示
分析
使用我這邊的幾個(gè)實(shí)際項(xiàng)目中的webview進(jìn)行測試,對于越“靜態(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)化相對沒有這么明顯,因?yàn)槲撮_啟情況下除了html均會被緩存。
- 未開啟LocalServer的非首次平均加載時(shí)間為142ms
- 開啟LocalServer的非首次平均加載時(shí)間為109.4ms
未開啟的最快的加載時(shí)間還會比開啟的快。由html的加載速度決定。
若是非純靜態(tài)頁面,開啟和未開啟的時(shí)間都會受到網(wǎng)絡(luò)狀況的影響,開啟LocalServer依舊有優(yōu)化效果,

未開啟LocalServer

開啟LocalServer
但可以看到靜態(tài)資源的讀取速度LocalServer下依舊比較快,而其他的資源則不穩(wěn)定了。
總結(jié)
對于打包到資源包中的資源,首次加載LocalServer可以有比較明顯的優(yōu)化效果,且速度比較穩(wěn)定,不會受到網(wǎng)絡(luò)波動的影響。
但是呢,使用了LocalServer,便無法使用瀏覽器自身的緩存,對于非首次情況優(yōu)化效果不大。
并且,LocalServer可能會有更新的問題,何時(shí)去檢查配置是否有更新?或許可以通過長鏈下發(fā)通知的方式,但沒有長鏈的話就得考慮下其他的方法來解決更新及時(shí)性的問題了。
Demo
Demo地址:github.com/EchoPuda/lo…
是個(gè)插件形式,可以直接使用。 有些東西可以根據(jù)業(yè)務(wù)調(diào)整,比如新增特殊的配置、資源包是否要分包、LocalServer的服務(wù)也可以根據(jù)url來開啟不同的服務(wù)等。
我是觸發(fā)預(yù)加載后會將下載成功或已經(jīng)成功的資源保存到內(nèi)存中,也可以在讀取時(shí)再進(jìn)行對應(yīng)的IO讀取文件,速度會相應(yīng)慢一點(diǎn)。
到此這篇關(guān)于Flutter WebView 預(yù)加載實(shí)現(xiàn) Http Server的方法的文章就介紹到這了,更多相關(guān)Flutter WebView 預(yù)加載內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
android實(shí)現(xiàn)圖片閃爍動畫效果的兩種實(shí)現(xiàn)方式(實(shí)用性高)
本文通過兩種方法給大家講解了android實(shí)現(xiàn)圖片閃爍動畫效果,實(shí)用性非常高,對這兩種方法感興趣的朋友一起通過本文學(xué)習(xí)吧2016-09-09
Android 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的動態(tài)交換以及數(shù)據(jù)交換,具體實(shí)現(xiàn)代碼跟隨小編一起看看吧2021-11-11
Android Tween動畫之RotateAnimation實(shí)現(xiàn)圖片不停旋轉(zhuǎn)效果實(shí)例介紹
Android中如何使用rotate實(shí)現(xiàn)圖片不停旋轉(zhuǎn)的效果,下面與大家共同分析下Tween動畫的rotate實(shí)現(xiàn)旋轉(zhuǎn)效果,感興趣的朋友可以參考下哈2013-05-05
利用Kotlin實(shí)現(xiàn)破解Android版的微信小游戲--跳一跳
這篇文章主要給大家介紹了關(guān)于利用Kotlin實(shí)現(xiàn)破解Android版微信小游戲--跳一跳的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-12-12
Android 自定義view模板并實(shí)現(xiàn)點(diǎn)擊事件的回調(diào)
這篇文章主要介紹了Android 自定義view模板并實(shí)現(xiàn)點(diǎn)擊事件的回調(diào)的相關(guān)資料,需要的朋友可以參考下2017-01-01

