2023年3月19日星期日

在 React Native WebView 中使用自定义字体

在 React Native 中使用自定义字体(自己提供 woff2 或其他格式的字体文件)很常见,官方文档也说得很清楚。在 WebView 中使用自定义字体也是有文档可以参考的。但在 React Native 里面使用 WebView 并且要在 React Native WebView 里面使用自定义字体,我搜索了一下没找到现成的文档,只好自己根据 iOS 和 Android 原生 WebView 的文档研究对应的 React Native 方法。

这里说的是在 React Native WebView 中嵌入构建时已经打包进去的本地自定义字体文件。如果使用网络上的自定义字体文件的话,标准的 CSS @font-face 就能解决,唯一需要注意的是网络上的自定义字体文件是否跨源(cross origin)。跨源在 WebView 里也不是解决不了的问题,可以通过改变页面的 baseUrl 把本地生成的 HTML 页面变成同源。也可以把字体部署到自己控制的服务器上,把 Access-Control-Allow-Origin header 设置好。

iOS

React Native WebView 在 iOS 上使用的是 WkWebView,React Native 中的组件 API 跟原生 WkWebView 的很相似。原生有一个这样的 API 用来加载一个 HTML 页面:

func loadHTMLString(
    _ string: String,
    baseURL: URL?
) -> WKNavigation?

原生应用可以通过 Bundle.main.bundleURL 来获得应用目录的绝对路径 URL,也就是以 file:// 开头的一个 URL。在 loadHTMLString 时把 baseURL 指向这个 URL 那打开页面的绝对路径也就是应用目录的绝对路径了。假设打包时已经把 custom-font.woff2 文件当作资源打包进去放在应用根目录了,那在 CSS 中就可以以相对路径的方式指向这个文件了:

@font-face {
  font-family: "Custom Font";
  src: url("custom-font.woff2") format("woff2");
}

在这里 custom-font.woff2 等同于 ./custom-font.woff2,也就是在应用根目录的同名文件。这在原生很容易解决的问题,在 React Native 中的主要障碍是缺乏一个 JavaScript 可以直接读取的 Bundle.main.bundleURL。为此我们要自己写一个简单调用 Bundle.main.bundleURLNative Module

// WebViewBaseUrl.m

#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>
@interface RCT_EXTERN_MODULE(WebViewBaseUrlModule, NSObject)
RCT_EXTERN_METHOD(getBaseUrl:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
+ (BOOL)requiresMainQueueSetup
{
  return YES;
}
@end
// WebViewBaseUrl.swift

import Foundation
@objc(WebViewBaseUrlModule)
class WebViewBaseUrlModule: NSObject {
  @objc
  func getBaseUrl(_ resolve: @escaping RCTPromiseResolveBlock,
                  rejecter reject: @escaping RCTPromiseRejectBlock) {
    resolve(Bundle.main.bundleURL.absoluteString)
  }
}

获取到应用路径后,把它用于 React Native WebView 的 baseUrl,本质上跟 iOS 原生的 baseURL 没什么区别,只不过前者是 string 后者是 NSURL

const baseUrl = await NativeModules.WebViewBaseURL.getBaseUrl();
return (
  <WebView
    source={{
      html,
      baseUrl,
    }}
  />
);

(考虑到 macOS 的相似性,这个方法估计在 macOS 上也有效,但我没有测试过。)

Android

Android 的应用根目录路径跟 iOS 不一样,此外 Android 给 WebView 提供了一个神奇的但仅限于 WebView 的应用根目录路径,那就是 file:///android_asset/。在 Android 的原生代码里面是不能使用 file:///android_asset/ 访问应用内的文件的,但在 WebView 里面却可以使用这个神奇的绝对路径。

有了这个神奇的路径后,Android 上需要做的事情就很简单了。我们不需要从原生代码获取应用路径,只需要使用准确的相对路径就可以了。假设 custom-font.woff2 文件正确地放置到了 assets/fonts/custom-font.woff2 目录,便宜打包后这个文件就可以在 WebView 里面通过 file:///android_asset/fonts/custom-font.woff2 访问。为了跟 iOS 使用相似的 CSS 相对路径,我们可以把 baseUrl 指向 file:///android_asset/fonts/(假设 WebView 不需要用到其他本地编译时打包好的资源的话)。

let baseUrl;
if (Platform.os === 'ios') {
  baseUrl = await NativeModules.WebViewBaseURL.getBaseUrl();
} else {
  baseUrl = 'file:///android_asset/fonts/';
}
return (
  <WebView
    source={{
      html,
      baseUrl,
    }}
  />
);

这样就可以同时覆盖 iOS 和 Android 的场景了。React Native WebView 其实还支持 Windows,但我不需要支持 Windows 所以也没去研究。

没有评论:

发表评论