2
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

posted at

updated at

ハイブリッドアプリでWebフォントをダウンロードせずアプリに梱包して使う方法

ハイブリッドアプリは起動が重い

ハイブリッドアプリは起動時に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を作成する」にチェックを入れて「変換開始」ボタンを押すだけ。

スクリーンショット 2018-07-08 18.35.19.png

「変換開始」を押すとアプリが固まるが、変換に時間がかかっているだけなのでしばらく待とう。

ファイルを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側に渡す。

iOSサンプル
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ディレクトリに置く。

スクリーンショット 2018-07-09 20.47.59.png

Androidサンプル
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が使えないかもしれない)

JavaScriptサンプル
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を指定すればフォントを使うことができる。

CSS
html, body {
   font-family: 'Noto Sans JP';
}

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
2
Help us understand the problem. What are the problem?