前提说明
在 Camera 模块中,我们提供了takePicture
和takeVideo
方法,这两个方法返回给
JS 端的都是本地图片视频资源的 uri,并且系统和系统版本不一样提供的 uri 也不一样。
这类 uri 在 WebView 中无法直接加载。
有一种方法,是直接读取资源转换成 base64,传递给 WebView 中读取和显示。但是缺点也很明
显,比如资源过大
,传递的base64过大
,转换成本
等等。
因此,前面Camera
和Image
模块使用的都是 uri,同时也引入一些问题。
这些 uri 由 native 返回,会包含以下协议种类:
- assets-library://...
- content://..
- file://...
但是 WebView 目前不支持加载这些协议,WebView 通常只支持 http 和 https,因此我们开着手 解决这个问题,让图片和视频都能加载并显示出来。
iOS
实践 iOS 端 WKWebView 如何加载本地资源,涉及到以下几点:
- WKWebView 的 WKURLSchemeHandler
- Photos: 苹果系统用于访问系统相册的资源
在使用到的代码上需要引入:
import Photos
import MobileCoreServices
WKURLSchemeHandler
在 WebViewController 的 viewDidLoad,添加如下一行代码:
let config = WKWebViewConfiguration()
...
// 注册assets-library协议的handler
config.setURLSchemeHandler(self, forURLScheme: "assets-library")
...
给 WebViewController 添加 extension,实现 WKURLSchemeHandler
extension WebViewController : WKURLSchemeHandler {
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
...
}
func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
...
}
}
WKURLSchemeHandler 是系统提供我们用于拦截浏览器请求的自定义 scheme 的资源,通过实现这个协议可以实现拦截:
extension WebViewController : WKURLSchemeHandler {
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
let req = urlSchemeTask.request
guard let url = req.url, let scheme = url.scheme else {
urlSchemeTask.didWKWebViewError(code: NSURLErrorUnsupportedURL)
return
}
}
func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
...
}
}
我们前面讲过了,iOS 的 JSBridge 的 Image 和 Camera 的模块返回的是:
- file://...
- assets-library://...
这两种资源,因此我们拦下这两种资源的请求,然后读取本地的资源返回,最终代码是:
extension WebViewController : WKURLSchemeHandler {
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
let req = urlSchemeTask.request
guard let url = req.url, let scheme = url.scheme else {
urlSchemeTask.didWKWebViewError(code: NSURLErrorUnsupportedURL)
return
}
// 如果scheme是相册的
if scheme == "assets-library" {
// 通过Photos框架,读取PHAsset
let result = PHAsset.fetchAssets(withALAssetURLs: [url], options: nil)
guard let asset = result.firstObject else {
// 如果不成功,返回失败
urlSchemeTask.didWKWebViewError(code: NSURLErrorResourceUnavailable)
return
}
// 出来图片
if asset.mediaType == .image {
let options = PHImageRequestOptions()
options.isNetworkAccessAllowed = true
options.deliveryMode = .highQualityFormat
options.isSynchronous = false
options.version = .current
// 请求资源数据
PHImageManager.default().requestImageData(for: asset, options: options) { data, _, _, _ in
guard let data = data else {
urlSchemeTask.didWKWebViewError(code: NSURLErrorResourceUnavailable)
return
}
let mimeType = asset.mimeType()
// WebView不支持heic,转成jpeg
if mimeType == "image/heic" {
guard let image = UIImage(data: data), let jpegData = image.jpegData(compressionQuality: 1.0) else {
urlSchemeTask.didWKWebViewError(code: NSURLErrorCannotDecodeRawData)
return
}
let resp = URLResponse(
url: url,
mimeType: "image/jpeg",
expectedContentLength: jpegData.count,
textEncodingName: nil)
urlSchemeTask.didReceive(resp)
urlSchemeTask.didReceive(jpegData)
urlSchemeTask.didFinish()
}
// 其他图片
else {
let resp = URLResponse(
url: url,
mimeType: mimeType,
expectedContentLength: data.count,
textEncodingName: nil)
urlSchemeTask.didReceive(resp)
urlSchemeTask.didReceive(data)
urlSchemeTask.didFinish()
}
}
}
// 视频处理
else if asset.mediaType == .video {
let options = PHVideoRequestOptions()
options.isNetworkAccessAllowed = true
options.deliveryMode = .highQualityFormat
options.version = .current
// 请求video的数据
PHImageManager.default().requestAVAsset(forVideo: asset, options: options) { avAsset, _, _ in
// 转成AVURLAsset,读取data
guard let urlAsset = avAsset as? AVURLAsset, let data = try? Data(contentsOf: urlAsset.url) else {
urlSchemeTask.didWKWebViewError(code: NSURLErrorCannotDecodeRawData)
return
}
// 返回
let mimeType = asset.mimeType()
let resp = URLResponse(
url: url,
mimeType: mimeType,
expectedContentLength: data.count,
textEncodingName: nil)
urlSchemeTask.didReceive(resp)
urlSchemeTask.didReceive(data)
urlSchemeTask.didFinish()
}
} else {
urlSchemeTask.didWKWebViewError(code: NSURLErrorResourceUnavailable)
}
}
}
func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
}
}
fileprivate extension WKURLSchemeTask {
func didWKWebViewError(code: Int) {
let error = NSError(domain: "WKWebViewError", code: code, userInfo: nil)
self.didFailWithError(error)
}
}
Android
实践 Android 端 WebView 如何加载本地资源,涉及到以下几点:
- WebViewClient 拦截资源请求
- ContentResolver:内容访问程序
扩展阅读:
内容提供程序 developer.android.com/guide/topic…
WebViewClient
还记得前面我们有一句代码:
webView.webViewClient = WebViewClient()
WebViewClient 是系统提供我们用于控制浏览器的行为,WebViewClient 类是系统提供的默认实现。我们 通过继承这个类可以实现很多控制,推荐大家网上搜索学习,这里直接进入正题:
继承 WebViewClient,需要提供参数 context,后面会使用到
class CustomWebViewClient(val context: Context) : WebViewClient() {
override fun shouldInterceptRequest(view: WebView, url: String): WebResourceResponse? {
...
}
}
使用新的 CustomWebViewClient
webView.webViewClient = CustomWebViewClient(this)
原理
我们覆盖shouldInterceptRequest
方法,这个方法可以让我拦截到这个 WebView 下的所有网络请求,
请求链接就是参数 url,拦下之后我们可以控制返回给 webView 的结果
但是,我们前面讲过了,Android 的 JSBridge 的 Image 和 Camera 的模块返回的是:
- file://...
- content://...
这两种资源,因此我们需要转换一个思路,由于我们可以拦截 http 的请求,那么我们把资源原本的链接拼到 http 的链接中,比如:
http://...?file=...
file 参数就是我的原链接,但是我们如何定义这个链接呢,简单的做法,使用一个基本不会被外界使用的域名:
加上资源链接为:content://media/240534,转换后的链接是:
file.local?file=content://media/2405…
那么加载图片时,使用:
<img src="http://file.local?file=content://media/240534" />
这个链接的请求会被拦截下来,file://
协议的资源同理
那么 Native 端的具体实现:
class CustomWebViewClient(val context: Context) : WebViewClient() {
override fun shouldInterceptRequest(view: WebView, url: String): WebResourceResponse? {
Log.e("WebView", "intercept $url")
val uri = Uri.parse(url)
// 解析拦截的url,判断域名
if (uri.host != "file.local") {
// 返回null,那么WebView会真的请求网络
return null
}
// 拿到file参数,否则也返回null
val filePath = uri.getQueryParameter("file") ?: return null
Log.e("WebView", "file $filePath")
// file协议
if (filePath.startsWith("file://")) {
// 去掉file://前缀创建File对象
val file = File(filePath.substring("file://".length))
// 通过ext得到mime
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(file.extension)
// 创建FileInputStream
val fis = FileInputStream(file)
// 返回本地的内容的Response
return WebResourceResponse(mimeType, StandardCharsets.UTF_8.name(), fis)
}
// content协议
else if (filePath.startsWith("content://")) {
// 创建Uri
val fileUri = Uri.parse(filePath)
// 获取mimeType
val mimeType = context.contentResolver.getType(fileUri)
// 通过contentResolver创建InputStream
val fis = context.contentResolver.openInputStream(fileUri)
// 返回本地的内容Response
return WebResourceResponse(mimeType, StandardCharsets.UTF_8.name(), fis)
}
// 其他情况
return null
}
}
JS
由于index.html
,这次我们使用新的页面去实现这个功能image.html
,内容如下:
<body>
<script src="./jsbridge.js"></script>
<script type="text/javascript">
function toLocalUrl(file) {
// android返回file.local域名的资源
if (window.androidBridge) {
return "http://file.local?file=" + encodeURI(file);
}
// iOS返回原始资源
else {
return file;
}
}
function onClickButton(button) {
var img = document.getElementById("image");
var video = document.getElementById("video");
switch (button.innerText) {
case "Camera.takePicture":
JSBridge.Camera.takePicture(result => {
if (result.success) {
JSBridge.UI.toast(result.file);
img.src = toHttpLocalUrl(result.file);
}
});
break;
case "Camera.takeVideo":
JSBridge.Camera.takeVideo(result => {
if (result.success) {
JSBridge.UI.toast(result.file);
video.innerHTML =
"<source src='" +
toHttpLocalUrl(result.file) +
"' type='" +
result.type +
"'>";
}
});
break;
case "Image.pickPhotos":
JSBridge.Image.pickPhotos(result => {
if (result.success) {
JSBridge.UI.toast(result.file);
if (result.type.indexOf("video/") === 0) {
video.innerHTML =
"<source src='" +
toHttpLocalUrl(result.file) +
"' type='" +
result.type +
"'>";
} else {
img.src = toHttpLocalUrl(result.file);
}
}
});
break;
}
}
</script>
<button onclick="onClickButton(this)">Camera.takePicture</button>
<button onclick="onClickButton(this)">Camera.takeVideo</button>
<button onclick="onClickButton(this)">Image.pickPhotos</button>
<img id="image" />
<video id="video" controls></video>
</body>
Android 改下链接为 image.html
class MainActivity : WebActivity() {
override fun getLoadUrl(): String {
return "http://192.168.31.101:8000/image.html"
}
}
运行效果:选择图片和视频后会显示出来。