<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
WebView 的文章分兩篇
本篇和大家一起討論下效能優化的問題。
WebView 頁面的體驗上之所以不如原生頁面,主要是因為原生頁面可以馬上顯示出頁面骨架,一下子就能看到內容。WebView 需要先根據 url 去載入 html,載入到 html 後才能載入 css ,css 載入完成後才能正常顯示頁面內容,至少多出兩步網路請求。有的頁面是用 js 渲染的,這樣時間會更長。要想讓 WebView 頁面能接近 Flutter 頁面的體驗,主要就是要省掉網路請求的時間。
做優化要考慮到很多方面,在成本與收益之間做平衡。如果不是新開專案,需要考慮專案當前的情況。下面分兩種情況討論一下。
頁面 html 已經在伺服器端拼接完成。只需要 html,css 就可以正常檢視頁面(主要內容不受影響)。如果你的專案的頁面是這樣的,那麼我們已經有了一個好的起點。
WebView 要顯示一個頁面,需要序列下面的過程。通過 url 載入到 html 後再載入 css,css 載入完成後顯示頁面。
url -> html -> css -> 顯示
我們可以對 css 的請求做一下優化。優化方案有兩種
第一種方案比較容易做,修改一下頁面的打包方案即可。很容易實現一份程式碼打包出兩個頁面,一個外連 css ,一個內聯css。但壞處也是很明顯的,每次都載入同樣的 css,會增加網路傳輸,如果網路不佳的話,對首屏時間可能會產生明顯的影響。就算拋開首屏時間,也會對使用者的流量造成浪費。
第二種方案可以解決 css 重複打包的問題。首先要考慮的問題是:css 放在原生的哪個地方?
有兩個地方可以放
檔案目錄用於儲存只能由該應用存取的檔案,系統不會清除該目錄,只有在刪除應用時才會消失。
從技術上來說,這兩種方案都是可以的。先說下不方便更新的問題:既然 app 的其它頁面都不能隨便更新,為什麼不能接受這個頁面的樣式不能隨便更新?如果是害怕版本衝突,那也好解決,發一次版,更新一次頁面地址,每個版本都有其對應的頁面地址,這樣就不會衝突了。根本原因是掌控的誘因,即使你能控制住誘因,你的老闆也控制不住。所以還是老老實實選第二種方案吧。
放哪裡的問題解決了,接下來要考慮的是如何更新 css 的問題。
因為有可能 app 啟動後第一個展示的就是這個頁面,所以要在 app 啟動後第一時間就更新 css。但又有一個問題,每次啟動都更新同樣的內容是在浪費流量。解決辦法是加一個設定,每次啟動後第一時間載入這個設定,通過設定資訊來判斷要不要更新 css。
這個設定一定要很小,比如可以用二進位制 01 表示true false,當然了可能不需要這麼極端,用一個 map 就好。
在 app 上啟動一個本地 http server 提供 css。 我們可以在打包的時候把 css 的外連寫成本地 http,比如 http://localhost:8080/index.css
。
除了 css,頁面的重要圖片,字型等靜態資源也可以放在本地,只要載入到 html 就可以立即顯示頁面,省了一步需要序列的網路請求。
到這裡伺服器端渲染頁面的優化就完成了,還是很簡單的吧,範例程式碼在後面。
近年來,隨著 vue,react 的興起,由 js 在瀏覽器中拼接 html 逐漸成為主流。雖然可以用同構的方案,但那樣會增加成本,除非必須,一般都是隻在瀏覽器渲染。可能你的頁面正是這樣的。我們來分析一下。
WebView 要顯示一個頁面,需要序列下面的過程。通過 url 載入到 html 後再載入 css、js,js 請求完資料後才能顯示頁面。
url -> html -> css,js -> js 去載入資料 -> 顯示
和伺服器端渲染的頁面相比,首次請求時間更長。多出了 js 載入資料的時間。除了要快取 css,還要快取 js 和資料。快取 js 是必須的,快取資料是可選的。好訊息是 html 只有骨架,沒有內容,可以連 html 也一起快取。
快取 js,html 的方案和快取 css 的方案是一樣的。快取資料會面臨資料更新的難題,所以只可以快取少量不需要時時更新的少量重要資料,不需要所有資料都快取。app 的原生頁面也是需要載入資料的,也不是每種資料都要快取。
資料更新之所以說是一個難題,是因為很多內容資料是需要即時更新的。但資料已經下發到使用者端,已經快取起來,使用者端不再發起新的請求,如何通知使用者端進行資料更新?雖然有輪詢,socket,伺服器端推播等方案可以嘗試,但開發成本都比較高,和獲得的收益相比,代價太大。
當快取了 html,css,js 等靜態資源後,h5 就已經和原生頁面站在同一起跑線上了,對於唯讀的頁面,體驗上相差無幾。
載入資料後還有js 拼接 html 的時間,和載入的時間相比,只要硬體還可以的情況下,消耗的時間可以忽略
圖片不適合用快取 css 的方案,因為圖片太大也太多。只能預載入少量最重要的圖片,其它大量圖片只能對二次載入做優化,我們會在後面討論
瀏覽器渲染的頁面也需要打包的配合,需要把所有的要快取的靜態資源地址都換成本地地址,這就要求釋出的時候一份程式碼需要釋出兩個頁面。一個是給瀏覽器用的,資源都通過網路載入。一個是給 WebView 用的,資源都從本地獲取。
思路已經有了,具體實現就簡單了。下面我給出關鍵環節的範例程式碼,供大家參考。
本地不需要 https,用 http 用行了,但是需要在 AndroidManifest.xml 的 applictation 中做如下設定 android:usesCleartextTraffic="true"
import 'package:shelf/shelf_io.dart' as io; import 'package:shelf_static/shelf_static.dart'; import 'package:path_provider/path_provider.dart'; Future<void> initServer(webRoot) async { var documentDirectory = await getApplicationDocumentsDirectory(); var handler = createStaticHandler('${documentDirectory.path}/$webRoot', defaultDocument: 'index.html'); io.serve(handler, 'localhost', 8080); }
createStaticHandler 負責處理靜態資源。
如果要相容 windows 系統,路徑需要用 path 外掛的 join 方法拼接
兩種方案:
相比之下,第 2 種方案都好一些。可以通過組態檔靈活修改哪些頁面需要快取。
在下面的範例程式碼中 ,cachedPagePaths
儲存著需要快取的頁面的 path。
import 'dart:async'; import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; class MyWebView extends StatefulWidget { const MyWebView({super.key, required this.url, this.cachedPagePaths = const []}); final String url; final List<String> cachedPagePaths; @override State<MyWebView> createState() => _MyWebViewState(); } class _MyWebViewState extends State<MyWebView> { late final WebViewController controller; @override void initState() { controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setNavigationDelegate(NavigationDelegate( onNavigationRequest: (request) async { var uri = Uri.parse(request.url); // TODO: 還應該判斷下 host if (widget.cachedPagePaths.contains(uri.path)) { var url = 'http://localhost:8080/${uri.path}'; Future.microtask(() { controller.loadRequest(Uri.parse(url)); }); return NavigationDecision.prevent; } else { return NavigationDecision.navigate; } }, )) ..loadRequest(Uri.parse(widget.url)); super.initState(); } @override void didUpdateWidget(covariant MyWebView oldWidget) { if(oldWidget.url!=widget.url){ controller.loadRequest(Uri.parse(widget.url)); } super.didUpdateWidget(oldWidget); } @override Widget build(BuildContext context) { return Column( children: [Expanded(child: WebViewWidget(controller: controller))], ); } }
如果頁面中有很多圖片,你會發現,體驗上還是不如 Flutter 頁面,為什麼呢?原來 Flutter Image Widget 使用了快取,把請求到的圖片都快取了起來。 要達到相同的體驗,h5 頁面也需要實現相同的快取功能。
關於 Flutter 圖片請參見 快速掌握 Flutter 圖片開發核心技能
要如何實現呢?只需要兩步。
第 1 條我舉個例子,比如圖片的地址為 https://s3.ap-northeast-1.wasabisys.com/img.it145.com/202302/logoeep2bliljj2.png
,打包的時候需要修改為 http://localhost:8080/logo.png
第 2 條的實現上,我們取個巧,借用 Flutter 中的 NetworkImage,NetworkImage 有快取的功能。
下面給出完整範例程式碼,貼到 main.dart 中就能執行。執行程式碼後看到一段文字和一張圖片。
注意先安裝相關的外掛,外掛的名字 import 裡有。
import 'dart:io'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'dart:async'; import 'dart:typed_data'; import 'package:shelf/shelf.dart'; import 'package:shelf/shelf_io.dart' as io; import 'package:shelf_static/shelf_static.dart'; import 'dart:ui' as ui; import 'package:webview_flutter/webview_flutter.dart'; const htmlString = ''' <!DOCTYPE html> <head> <title>webview demo | IAM17</title> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no,viewport-fit=cover" /> <style> *{ margin:0; padding:0; } body{ background:#BBDFFC; text-align:center; color:#C45F84; font-size:20px; } img{width:90%;} p{margin:30px 0;} </style> </head> <html> <body> <p>大家好,我是 17</p> <img src='http://localhost:8080/tos-cn-i-k3u1fbpfcp/ c6208b50f419481283fcca8c44a2e3af~tplv-k3u1fbpfcp-watermark.image'/> </body> </html> '''; void main() async { runApp(const MyApp()); } class MyApp extends StatefulWidget { const MyApp({super.key}); @override State<MyApp> createState() => _MyAppState(); } class _MyAppState extends State<MyApp> { WebViewController? controller; @override void initState() { init(); super.initState(); } init() async { var server = Server17(remoteHost: 'p6-juejin.byteimg.com'); await server.init(); var filePath = '${server.webRoot}/index.html'; var indexFile = File(filePath); await indexFile.writeAsString(htmlString); setState(() { controller = WebViewController() ..loadRequest(Uri.parse('http://localhost:${server.port}/index.html')); }); } @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: SafeArea( child: controller == null ? Container() : WebViewWidget(controller: controller!), ), )); } } class Server17 { Server17( {this.remoteSchema = 'https', required this.remoteHost, this.port = 8080, this.webFolder = 'www'}); final String remoteSchema; final String remoteHost; final int port; final String webFolder; String? _webRoot; String get webRoot { if (_webRoot == null) throw Exception('請在初始化後讀取'); return _webRoot!; } init() async { var documentDirectory = await getApplicationDocumentsDirectory(); _webRoot = '${documentDirectory.path}/$webFolder'; await _createDir(_webRoot!); var handler = Cascade() .add(getImageHandler) .add(createStaticHandler(_webRoot!, defaultDocument: 'index.html')) .handler; io.serve(handler, InternetAddress.loopbackIPv4, port); } _createDir(String path) async { var dir = Directory(path); var exist = dir.existsSync(); if (exist) { return; } await dir.create(); } Future<Uint8List?> loadImage(String url) async { Completer<ui.Image> completer = Completer<ui.Image>(); ImageStreamListener? listener; ImageStream stream = NetworkImage(url).resolve(ImageConfiguration.empty); listener = ImageStreamListener((ImageInfo frame, bool sync) { final ui.Image image = frame.image; completer.complete(image); if (listener != null) { stream.removeListener(listener); } }); stream.addListener(listener); var uiImage = await completer.future; var pngBytes = await uiImage.toByteData(format: ui.ImageByteFormat.png); if (pngBytes != null) { return pngBytes.buffer.asUint8List(); } return null; } FutureOr<Response> getImageHandler(Request request) async { if (RegExp( r'.(png|image)$', ).hasMatch(request.url.path)) { var url = '$remoteSchema://$remoteHost/${request.url.path}'; var imageData = await loadImage(url); //TODO: 如果 imageData 為空,改成錯誤圖片 return Response.ok(imageData); } else { return Response.notFound('next'); } } }
第 2 條。本例中是直接存取的 localhost,實際應用中,頁面地址是外連地址,通過攔截的方式請求本地。如何做頁面地址攔截前面已經給出範例了。
第 3 條。打包後的時候對所有圖片地址都寫成了本地地址,改成本地地址的目的就是為了讓圖片請求都由本地 server 響應。本地 server 拿到 圖片地址後,再改回網路地址,通過 NetworkImage 請求圖片。NetworkImage 會首先判斷有沒有快取,有直接用,沒有就發起網路請求,然後再快取。
可能你覺得有點繞,既然最後還要用網路地址,為什麼還要先寫成本地地址,象攔截頁面請求那樣攔截圖片請求不香嗎?答案是不可以。兩個原因。
對比於攔截 443 埠,修改打包方案要容易的多。
在範例程式碼中,用 RegExp( r'.(png|image)$',)
判斷是否要響應請求。從正則可以看出,以 png 或 image 結果的圖片都能響應請求。判斷 image 是因為範例中的圖片地址是以 image 結尾的。
範例程式碼只能支援 png 格式的圖片,範例圖片雖然是 image 結尾,但格式也是 png 格式。如果要支援更多格式的圖片,需要用到第三方庫。
如果圖片地址失改,可以自行換一個,隨使在網上找個 png 圖片 地址就行。
我們演示了把圖片快取到記憶體,當 app 被殺掉,快取都沒了,除非快取到磁碟。這項工作已經有外掛幫我們做了。 用 cached_network_image 替換 NetworkImage,稍加改動就可以實現磁碟快取了。
如果不做圖片快取,通過前面兩個方案,h5 速度就已經得到大大提高了。如果有餘力,可以做圖片快取。圖片快取是可選的,是對前面兩種方案的加強。
可能你的專案不同,有不同的方案,歡迎一起討論。
本文到這裡就結束了,謝謝觀看。
為了給自己一點壓力,上一篇 在 Flutter 中使用 webview_flutter 4.0 | js 互動 中我就預告說今天要發這篇效能優化的文章。結果壓力是有的了,但卻沒能按時完工(理想情況是週日下午完工,這樣可以休息一下)。一個原因是 升級 flutter 報錯,浪費了一個上午,再有就是寫了一版後,並不滿意,又重寫了一版,最後才定稿。一直寫到深夜才把主要內容寫完。早上起來又做了補充修改。
以上就是Flutter WebView效能優化使h5像原生頁面一樣優秀的詳細內容,更多關於Flutter WebView頁面優化的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45