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 所以也没去研究。

2023年3月6日星期一

工程师成长到最后最重要的是什么?

有一个问题我最近在 career coaching 时连续被不同的人问了几次,第一次被问到时我想到什么就说什么,但被连续问了几次之后我觉得我需要好好思考一下。这个问题问的是,「工程师成长到最后,最重要的是什么?」

刚刚进入互联网行业的新人,往往觉得自己懂得还不够多,别人比自己厉害、比自己资深,是因为别人比自己懂得更多。按照这个逻辑进行推导,正确的目标自然是让自己懂得越来越多。再往前推导,既然每一个人的时间是有限的,那必须优先弄懂那些重要的事情,因此有了上述这个问题。大家都不想走弯路,希望找到直达终点的直线。


我觉得这个问题的答案并不是清晰可知的,因为清晰可知的事情迟早会被大家优化到极点。能够通过面试进入大厂的人,都聪明和勤奋到一定程度。如果存在这样一条直线,大家都或多或少地都沿着这条直线全力往前跑,最终跑出来的结果会存在一定的差别但不会存在巨大的差别,尤其是不会存在那种走直线比走曲线所应该获得的巨大优势。

那真正能够产生差异的是什么?那必然是一些别人没办法直接告诉你的事情。这是可以通过反证法证明的。如果有人能够教会你如何能够获得别人不能获得的巨大优势,他会选择教你吗?为什么?大家很容易产生一种错觉,「比我厉害的人肯定懂得比我多,这跟在学校里一样,但是他们不愿意教我所以我不如他们厉害。他们联合起来保守秘密,阻止我们获得他们拥有的知识」。

按照这样的假设,比你厉害的人当中只要有一个人守不住秘密,这种知识早就传播开来了。而且教授这种知识肯定是能赚钱的,所以比你厉害的人其实都有动机通过教授这种知识来赚钱,他们不可能真的联合起来保守秘密。这就可以反证,不是别人拥有你没有的知识而且不愿意教你,现实更有可能是他们没办法教你,或者说教你的成本太高所以不知道花时间去教。


有一个概念叫做「隐性知识」,英文叫做「tacit knowledge」,指代的是那些难以言述的知识。隐性知识类似于通过深度机器学习训练出来的一个模型:它可以用,它大多数时候是正确的,少数时候会出错。你没办法通过人和人之间很容易就能沟通的若干条规则把它描述清楚,因为能描述清楚的话就不需要用到机器学习了,用规则系统就可以了。你没办法很好地预测它什么时候对什么时候错,你也不太能解释它错的时候为什么出错了。

面对这样的模型,你可以选择把它整个拷贝走。但如果你不能拷贝呢?(因为人类还没有进行人脑到人脑拷贝的技术,也不确定这样的技术是否真能被发明出来。)如果你能使用这个模型但不能拷贝这个模型,你可以用这个模型作为基准和反馈从零开始训练一个相似的模型。这也是现在已知的传播隐性知识的方法:找一个把某件「只能意会不能言传」的事情做得非常好的人,你在进行训练时请他提供反馈,最终你训练出来的结果不可能跟他完全一样,但你的目标也只是获得相似效果而已。至于能否训练出来,以及要训练多久才能达到你想要的效果,这都是要尝试过才知道的。


现实就是这样子的:有能力进入大厂的人,通过过书本(或其他媒体)学习显性知识的效率肯定不会差。瓶颈往往出现在显性知识能学的都学了,学更多也不能变得更厉害了,但又没有意识到需要甚么样的隐性知识,或者是不知道从哪里能够获得需要的隐性知识。

所以工程师成长到了一定的阶段以后,搞明白自己需要什么以及找到跟谁能获得有效的训练是最重要的。具体怎么做就不是通过写一篇文章就能说清楚的,否则这就不叫做隐性知识了。我能给的建议是:找你相信真心关注你成长的人,请他们花时间观察你工作,问他们觉得你的成长需要什么、谁能够帮助你训练你所需要的。