ハイブリッドアプリは起動が重い
ハイブリッドアプリは起動時にJavaScript、CSSなどのリソースをガバっととるから起動が重くなりがち。
中でも日本語Webフォントはひときわ重く、一つのタイプ(レギュラー、ボールドなど)だけで数メガバイトある。
東京にいると気づきづいらいが、日本でも未だに3G回線の人はまあまあいる。
3G回線で数メガバイトのダウンロードは非常に重く、日本語Webフォントの通常使用は現実的ではない。
フォントをダウンロードせずネイティブアプリに梱包する
この問題を解消するため、日本語Webフォントはダウンロードするのではなく、ネイティブアプリに梱包したものを使うようにする。
しかしWebViewからそのままローカルのリソースは参照できないので、Base64でローカルからWebViewにフォントデータを連携してやる必要がある。
(Androidはhtmlをassetsに置くと一応ローカルリソースを参照できるが)
おすすめはしない
この方法は結構強引な上に手順が多く面倒くさい。
色々と面倒なので、現状ではそもそもハイブリッドアプリに日本語Webフォントを使うことをお勧めしない。
フォントファイルの準備
ここから実践。
フォントのダウンロード
フォントはNoto Sans JPを使う。
リンク先のDownload
から落としたzipを解凍して、使用するフォントファイルを取り出す。
実用にはRegularとBold2種類のフォントがあった方が良いと思うが、ここでは簡単にするためRegularフォントのみ扱う。
- NotoSansJP-Regular.otf (2.3MB)
ちなみにNoto Sans CJK JPというフォントもあるが、こちらはおそらく含む文字が多くサイズがでかすぎるので、そのまま使うのは厳しい。
フォーマットをwoff2に変換する
落としたファイルはotf
のフォーマットなのでWebで使えるwoff2
の形に変換する。
変換にはWOFFコンバータというツールを使わせて頂いた。
使い方は非常に簡単で、otfファイルを設定したら「WOFF2を作成する」にチェックを入れて「変換開始」ボタンを押すだけ。
「変換開始」を押すとアプリが固まるが、変換に時間がかかっているだけなのでしばらく待とう。
ファイルをBase64に変換する
上で作成したwoff2ファイルをBase64形式に変換する。
WebにBase64変換ツールはたくさんあるが、数MBのバイナリを変換できるものは意外と少ない。
変換にはこちらのエンコーダーを使用した。
ファイルでダウンロードできるとありがたいが、変換結果はブラウザにテキストで出力されるので、コピーしてエディターに貼り付けて保存する。
ファイル名は任意で良いが、ここでは以下のようにしておく。
- NotoSansJP-Regular.woff2.base64 (2.3MB)
ネイティブアプリの実装
Cordovaなど使うフレームワークによって変わってくるが、ここでは素のWebViewで実装する。
ネイティブ側がやるのはBase64のフォントデータをWebViewのJavaScript側に渡すこと。
JavaScript側はwindow.setBase64FontData()
でデータを受け取る前提にしているので、ネイティブ側でこの関数を呼び出す。
iOSの実装
Base64にしたフォントファイルは、ドラッグアンドドロップでプロジェクトに突っ込めばOK。
あとは、JavaScript側からtransferFontData
という名前でネイティブの処理を呼び出せるようにして、呼び出されたらwindow.setBase64FontData
でBase64のテキストをJavaScript側に渡す。
class ViewController: UIViewController, WKScriptMessageHandler {
var webView: WKWebView?
override func viewDidLoad() {
super.viewDidLoad()
let contentController = WKUserContentController()
contentController.add(self, name: "transferFontData")
let config = WKWebViewConfiguration()
config.userContentController = contentController
let webView = WKWebView(frame: CGRect.zero, configuration: config)
self.webView = webView
view.addSubview(webView)
webView.translatesAutoresizingMaskIntoConstraints = false
webView.leadingAnchor.constraint(equalTo:view.leadingAnchor).isActive = true
webView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
// SafeAreaを使っているのでiOS10未満に対応するなら多少修正が必要
webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
webView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let url = URL(string: "http://192.168.2.127:8020") {
webView?.load(URLRequest(url: url))
}
}
/// WebViewからのJavaScript呼び出しをHandleする
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
transferFontData()
}
/// フォントデータをJavaScript側に渡す
func transferFontData() {
if let path = Bundle.main.path(forResource: "NotoSansJP-Regular.woff2", ofType: "base64"),
let base64 = try? String(contentsOfFile: path) {
let script = "window.setBase64FontData('\(base64)');"
webView?.evaluateJavaScript(script, completionHandler: nil)
}
}
}
Androidの実装
フォントファイルはassetsディレクトリに置く。
public class MainActivity extends AppCompatActivity {
WebView webView = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
webView = findViewById(R.id.mainWebView);
webView.getSettings().setJavaScriptEnabled(true);
webView.addJavascriptInterface(this, "Android");
webView.loadUrl("http://192.168.2.127:8020");
}
@JavascriptInterface
public void transferFontData(final String[] args) {
runOnUiThread(new Runnable() {
@Override
public void run() {
try {
String base64 = readAssetString(getAssets(), "NotoSansJP-Regular.woff2.base64");
String script = "window.setBase64FontData('" + base64 + "');";
webView.evaluateJavascript(script, null);
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
});
}
private static String readAssetString(AssetManager asset, String fileName) throws IOException {
InputStream input = null;
BufferedReader reader = null;
StringBuilder builder = new StringBuilder();
try {
input = asset.open(fileName);
reader = new BufferedReader(new InputStreamReader(input));
String line;
while ((line = reader.readLine()) != null) {
builder.append(line);
}
} finally {
if (input != null) input.close();
if (reader != null) reader.close();
}
return builder.toString();
}
}
Web(JavaScript)の実装
window.setBase64FontData
でネイティブからBase64のフォントデータを受け取り、StyleSheetのfont-faceに追加する。
本連携方法は、font-faceのURLにBase64でデータを直書きできる仕様により成り立っている。
フォントデータが読み込まれるまで待機する必要があるが、そこらへんのコードはちょっと怪しい。
(Android5未満はdocument.fontsが使えないかもしれない)
class Launcher {
start() {
this.postToNative('transferFontData');
this.waitFont();
}
// ネイティブアプリの処理を呼び出す
postToNative(name, param) {
let handler = undefined;
if (window !== undefined && window.webkit !== undefined && window.webkit.messageHandlers !== undefined) {
handler = window.webkit.messageHandlers;
} else if (typeof Android !== "undefined") {
handler = Android;
}
if (handler !== undefined && handler[name] !== undefined) {
if (handler[name].postMessage !== undefined) {
handler[name].postMessage(param);
} else {
handler[name](param);
}
}
}
// フォントデータが読み込まれるのを待つ
waitFont() {
let fontSet = document.fonts;
if (fontSet !== undefined) {
fontSet.load(`20px "Noto Sans JP"`); // 20pxは適当。サイズはたぶんなんでもいい?
fontSet.ready.then((fontFaceSet) => {
this.onFontLoaded();
});
} else {
this.onFontLoaded();
}
}
// フォントデータ読み込み完了時の処理
onFontLoaded() {
// 画面を表示
}
}
// Base64のフォントデータをセットする(ネイティブから呼び出し)
window.setBase64FontData = (base64) => {
let style = document.createElement('style');
style.appendChild(document.createTextNode(`
@font-face {
font-family: 'Noto Sans JP';
src: url('data:font/woff2;base64,${base64}');
font-weight: normal;
}
`));
document.head.appendChild(style);
};
new Launcher().start();
※JavaScriptのコードはES6なので、Babelを使って古いブラウザでも動くよう変換している。
あとはCSSでフォントにNoto Sans JPを指定すればフォントを使うことができる。
html, body {
font-family: 'Noto Sans JP';
}