くらしのマーケット開発ブログ

「くらしのマーケット」を運営する、みんなのマーケット株式会社のテックブログです

あの日呼び出したモジュールの名前を僕達はまだ知らない

はじめに

みんなのマーケットで iOS / Android アプリエンジニアとして働いているYangです。

非常に分(意)か(味)り(不)や(明)す(!)く(!)タイトルに書いた通り、今回はデカップリングを話題として、Androidのモジュール化を紹介したいと思います。

モジュール化とは

イメージとしては:

f:id:curama-tech:20180608115351j:plain

(モザイク除去はこちらへ画像の出典)

違う!

複雑で巨大なシステムやプロセスを設計・構成・管理するとき、全体を機能的なまとまりのある“モジュール”に要素分割すること。設計・製造時の擦り合わせ作業をできるだけ少なくするために構成要素(部品)の規格化・標準化を進め、その相互依存性を小さくすることをいう。出典

「wwwの生みの親」「Webの父」などの異名を持つ、ティム・バーナーズ=リーさんの本「Principles of Design」に以下の名言があります。

Principles such as simplicity and modularity are the stuff of software engineering; decentralization and tolerance are the life and breath of Internet.

会社の事業拡大と共に、アプリの機能もどんどん増えて、複雑になっています。

巨大とまではいかなくてもビルドに時間はかかるし、ファイルツリーがだんだん長くなって、開発もしづらく、依存が多すぎて単体テストが書きづらいです。また、部分的な不具合や変更が発生したときに、その影響が全体へと波及してしまいます。

解決しないといけないこと

1. コードのデカップリング(分離原則)

 どうやって一つの巨大のプロジェクトを複数のモジュールに分けられますか。もしモジュール間で、再度お互いに直接アクセスするのであれば、デカップリングとは全く言えません。どうすれば直接の参照を避けられるでしょうか。

2. 各モジュールの独立実行

 一回のデバッグにもし一つか二つぐらいのモジュールだけ参加させたら、ビルドの時間は大幅に短縮できますから、開発の段階でどうやって最低限必要なモジュールだけをコンパイルさせますか。各モジュールが疎結合であれば,どうやって独立に実行やデバッグをさせますか。

3. 各モジュール間のデータ通信

 各モジュールは外部へのサービスを提供することができますが、メインモジュール(Host)や他のモジュールへどうやってデータを送受信すればいいでしょうか。

 実際画面遷移の本質も特殊なデータ送信ですが、Android上の画面遷移は Intent を使わないといけないので、解決の方法は少し違います。

コードのデカップリングの解決

巨大なコードを分割しやすくするために、Android Studio IDEに Multiple Module という機能をサポートしていて、これを使って最初に簡単な分離はできます(つまり参照が複雑なところは自分で整理しないといけないです)。

ここで明確したい概念があって、モジュールは二種類に分けることができます。

一つは Base Library で、これらは他のモジュールから直接参照できます。例えば、ネットワークモジュールや画像処理モジュールは二つの Base Library として考えられると思います。

もう一つは Component と似ているような感じで、完全な機能を持っています。くらしのマーケットのアプリを例にすると、予約モジュールやチャットモジュールは二つの Component と呼ばれたらいいです。

Base Library は単純に汎用的なサービスを提供するため、 モジュール化 に言った モジュール は基本的に二つ目です。

これで、コードの分離は図の通りにできます。

f:id:curama-tech:20180608115345p:plain

矢印を書いているところだけ依存関係が存在して、つまり Reservation ModuleCalendar Module はお互いに見えないです。こうすると、コードのデカップリングを実現できます。

各モジュールの独立実行の解決

ビルドツールに対して、あるモジュールがプロジェクトの入り口かどうかの判断は該当モジュールの build.gradle を参照しています。

apply plugin: 'com.android.library' の場合は部品で、 apply plugin: 'com.android.application' の場合はアプリと識別されます。

つまり一個 isRunAlone の変数を用意すれば、切り替えは可能です。

これ以外、アプリと識別された場合、 AndroidManifest.xml ファイルに最初ページとしての Activity も指定する必要があります。私の解決方法では二つのManifestファイルを用意して、リリースの際にdebug用のファイルを除外します。

最後、アプリとして実行するために必要な applicationId も指定して、独立実行の準備はできました。

if (isRunAlone.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}
// ......
    defaultConfig {
        if (isRunAlone.toBoolean()) {
            applicationId "jp.curama.shop.module1"
        }
    // ......
    }
// ......
    sourceSets {
        main {
            if (isRunAlone.toBoolean()) {
                manifest.srcFile 'src/main/debug/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/release/AndroidManifest.xml'
                java {
                    exclude 'debug/**'
                }
            }
        }
        // ......
    }
// ......

各モジュール間のデータ通信の解決

前書いた通り、ここは二つのシーンがあって、①画面遷移と②メソッド呼び出しです。色々調べて、一番良い解決案はAlibabaが提供している Open Source Library ARouter です。一発で二つのシーンを解決できます。

Android の標準画面遷移は下の通りです。ここでの問題は、 ActivityAActivityB をインポートしないと遷移できません。

// filePath: module1/ActivityA.kotlin
// module2への依存が発生してしまいました
import module2.ActivityB

class ActivityA: BaseActivity() {
    fun navigateToB(id: String) {
        val intent = Intent(this, ActivityB::class.java).apply { putExtra("id", id) }
        startActivity(intent)
    }
}

// filePath: module2/ActivityB.kotlin
class ActivityB: BaseActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState);
        val id = intent?.getStringExtra("id")
        // ......
    }
}

ARouterを使う場合、 ActivityA は遷移先の URL がわかったら遷移できます。私達のシステムでは管理上の考えによって、 RouterPath という URL の集約ファイルを作って、 Base Business Layer に置いて、 URL のハードコードを避けました。またどの画面がどのURLを使っていて、必要なパラメーターが何であるかは誰でもすぐに一目瞭然で、把握できます。もう一つ便利なところは ARouter 経由で渡したパラメーターがインジェクターによって自動的に遷移先へアサインされるところです。

// filePath: module1/ActivityA.kotlin
// import ActivityB は不要です
class ActivityA: BaseActivity() {
    fun navigateToB(id: String) =
        ARouter.getInstance().build("module2/b").withString("id", id).navigation()
}

// filePath: module2/ActivityB.kotlin
@Route(path = "module2/b")
class ActivityB: BaseActivity() {
    @Autowired
    lateinit var id: String

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState);
        ARouter.getInstance().inject(this);
        // ここから this.id はもう使えます
        // ......
    }
}

シーン②も似たやり方で、 URL でアノテーションをして、使用者はインタフェースに依存することで、ARouterから実現を取得して、使ったらうまく動けます。

これでモジュール化の大きい問題を全部解決できました。

最後に

我々みんなのマーケットテックチームでは「くらしのマーケット」を一緒に作る仲間を募集しています!どんな環境で開発しているかはこちらの記事にまとまっています。興味がある方はぜひ気軽に連絡ください (コーポレートサイト https://www.minma.jp/

次回は、SREチームの千代田さんの予定です。

本番サーバーが結構な頻度で再起動を繰り返していたのでNode.jsのメモリ調査をしてみた話

みんなのマーケットでwebエンジニアとして働いている高橋です。

前回の記事(http://tech.curama.jp/entry/2018/02/23/120000)で、弊社ではNode.jsを使用しているという話を簡単にしましたが、今回はその運用中に出てきたトラブルシューティングの話をしたいと思います。

弊社ではマイクロサービス用のBFF(Backends For Frontends)としてNode.jsを採用しています。 ところが最近、弊社SREから、「Nodeサーバーの再起動が多すぎます。メモリが増えた時に再起動で解決するんじゃなくて、ちゃんとリークしないように最適化してください」というお願いがありました。

また、こちらの記事には、

As leaks grow, V8 becomes increasingly aggressive about garbage collection, slowing your app down.

とありますので、メモリリークを再起動で回避し続けることには、問題がありそうです。というわけで、実際にヒープダンプを取ってメモリ周りの調査を行ってみることにしました。(対処法については、主に*1を参考にしました。)

サーバーのメトリクス

まずは本番サーバの状態を見てみます。弊社ではgrafanaを用いてcpu使用率やメモリ使用量を可視化しています。今回問題があるサーバの状態を見てみると、f:id:curama-tech:20180523200256p:plainのように、数十分おきにメモリ量が変動していることがわかります。定期的にメモリ使用量が減っているのは、Node.jsのプロセスマネージャであるPM2によって、メモリが増えた場合にプロセスの再起動を行うように設定していたためです。

ローカル環境で挙動の確認

次にローカル環境で挙動を確認してみます。PM2にはpm2 monitというモニタリング用のコマンドがあります。これを実行しながらリクエストを投げてみると、 1リクエストあたり数MBから数十MBメモリ使用量が増えることがわかりました。また、リクエストのない状態にしてからメモリ使用量のベースラインに戻るまで、30sから60s程かかりました。また、リクエストを繰り返すと、ベースラインが次第に上がっていくことも観測できました。

強制ガベージコレクションを行ってみる

Node.jsは、--expose-gcフラグを付けて起動することによって、global.gc()として、ガベージコレクションを手動で実行することができます。試しに手動でこれを実行してみました。

状態 メモリ使用量
ベースライン 85MB
リクエスト10回実行 111MB
ガベージコレクション実行後 108MB

ですが、ガベージコレクションによって、たった3MBしかメモリ使用量が減少していません。この時点で、メモリリークが発生している可能性が示唆されました。

メモリリークの調査をしてみる

リークの調査には、まずはじめにmemwatch-nextを使用しました。

npm install memwatch-next

した後、

import * as memwatch from "memwatch-next";

memwatch.on("leak", info =>  {
    console.log("leak! ☔", info);
});

memwatch.on("stats", stats =>  { 
    console.log("stats! 📺", stats);
});

const hd = new memwatch.HeapDiff();
setInterval(() => {
    const diff = hd.end();
    console.log(diff);
}, 1000);

のようにして、ガベージコレクションのイベントが発生した場合にヒープのログを見ることができます。

この時点で手動でガベージコレクションを発生させると、

0|www      | stats! 📺 { num_full_gc: 2,
0|www      |   num_inc_gc: 19,
0|www      |   heap_compactions: 2,
0|www      |   usage_trend: 0,
0|www      |   estimated_base: 75128928,
0|www      |   current_base: 75128928,
0|www      |   min: 0,
0|www      |   max: 0 }

のようなログをコンソールから見ることができます。

更に試しに、

class Fuga {
    private foo: string[];
    constructor() {
        process.stdout.write("⏳");
        this.foo = [];
        for (let i = 0; i < 1000; i++) {
            const data = Math.random().toString();
            this.foo.push(data);
        }
    }
}

var leakyData: Fuga[] = [];

const hoge = () => {
    leakyData.push(new Fuga());
};

setInterval(() => {
    hoge();
}, 1);

のようにglobalから参照可能なオブジェクトに大量にデータを詰め込むと、

leak! ☔ { growth: 1374330784,
  reason: 'heap growth over 5 consecutive GCs (1m 4s) - -2147483648 bytes/hr' }

というように、leak時の警告を見ることができます。

ここまで仕込んでから、ローカルのサ−バーに対して

 while true; do curl localhost:3000; sleep 1s; done

として1sおきにリクエストを投げてみます。

しかし、ローカルサーバーではleakイベントが発生しませんでした。

次に、ヒープダンプのリクエスト毎のdiffを確認します。

ローカルのサーバーに対して数回リクエストを送り、new memwatch.HeapDiff();を利用したdiffを見てみると、

0|www      | { what: 'Code',
0|www      |   size_bytes: 527632,
0|www      |   size: '515.27 kb',
0|www      |   '+': 2448,
0|www      |   '-': 1510 }
0|www      | { what: 'Array',
0|www      |   size_bytes: 244784,
0|www      |   size: '239.05 kb',
0|www      |   '+': 8233,
0|www      |   '-': 6538 }
0|www      | { what: 'String',
0|www      |   size_bytes: 52816,
0|www      |   size: '51.58 kb',
0|www      |   '+': 1030,
0|www      |   '-': 386 }
0|www      | { what: 'Closure',
0|www      |   size_bytes: 11592,
0|www      |   size: '11.32 kb',
0|www      |   '+': 217,
0|www      |   '-': 55 }
0|www      | { what: 'Object',
0|www      |   size_bytes: 7944,
0|www      |   size: '7.76 kb',
0|www      |   '+': 453,
0|www      |   '-': 279 }

Codeというオブジェクトが最もサイズが大きいことがわかりました。

これは、v8で定義されており、「コンパイル済みのコードに関連するすべてのオブジェクトです」ということです。

このオブジェクトはJITで生成されるものなので、アプリケーション固有のものではありません。ですので次に、より詳細なヒープの内容を追うことにしました。

詳細なヒープダンプを取得する

詳細なヒープダンプを取得するために、nodeを--inspectオプション付きで起動します。

すると、

chrome-devtools://devtools/remote/serve_file/xxxxx

というリンクがコンソールに表示されるので、chromeでそのURLにアクセスします。

その後、Profilesタブから、Take Heap Snapshotを選択すると、ヒープダンプが取得できます。

試しに、

a) 初期状態
b) 5回リクエス
c) 5分後

としてダンプを取得してみました。ここから、Summary -> Comparisonと選択し、ヒープのdiffを取ることができます。

詳しいダンプの見方はこちらです。

この結果、動かしていたアプリケーションのダンプの中で、リクエストに応じて増え、ガベージコレクトされないオブジェクトを特定することができました! (具体的には、利用していたDependency Injection用のパッケージが原因でした)

f:id:curama-tech:20180530111246p:plain
犯人

最後に

以上のようにして、Node.jsのメモリ調査をしてみました。今までNode.jsのガベージコレクションの仕組みを意識することがなかったので、知見が溜まってよかったです。

(次回はアプリエンジニアのヤンさんの予定です!)

みんマのいろんな部署を経てテックチームにきてよかった話

こんにちは!みんマテックチームのめぐみです。

社内では会社名と運営しているサービスのことを略して
会社名:みんなのマーケットのことを→「みんマ」
運営しているサービス:くらしのマーケットのことを→「くらマ」
と呼んでいます。この記事では略して書いていきます。


(くまモンと仲良し感のある猫の写真に最近癒されています)

私はみんマで働いて2年半、そのうちテックチームで働いて1年です。テックに入る前までは採用担当、コンサル、ちょっとマーケの業務をしていました。

今はテックチームでカテゴリーマネージャーをやっています。仕事している上で、いろんな部署で働いていたことがあってよかったと感じることがあるのでその話をしていきます。

ところで、カテゴリーマネージャーとはなにか?

くらしのマーケットにはハウスクリーニング系カテゴリやリフォーム系カテゴリなど、200個近いカテゴリが存在しています。そのうち、私はリフォーム系カテゴリの流通額を増やすことにコミットしています。 このままだとわかりにくいので、仕事内容をちょっと紹介します。

カテゴリーマネージャーの仕事をちょっと紹介

他部署で働いていたことの何がよかったのかを話す前に、カテゴリーマネージャーとしてやっていることの一部を紹介します。 最終的には流通額をあげることをゴールに置いていますが、まずは課題と感じていたコンサルティング業務の業務改善から進めています。

コンサルティング業務の効果の見える化

みんマには、くらしのマーケットに出店している出店者の売上をあげるためにアドバイスなどをするECコンサルタントがいます。しかし、実際のところコンサルティングしている内容がどれくらい実行されたか、また売上に貢献したかというのが今まであまり可視化されていませんでした。 そこでコンサルティング結果の見える化と、効果のあるコンサルティングの再現性を高くすることを目的に効果測定を開始しました。

コンサルティング業務の改善

効果測定の結果わかったことをもとに、下記を行いました。

  • 新しいコンサルティング方法を導入(オンラインコンサルティング)
     なぜ提案が実行されないのか?どういう時だったら実行度が高いのか?を分析した結果、導入を決定しました。導入後の結果についてはまだ導入したばかりなのでこの記事では触れません。

  • 売上アップに繋がらない業務の排除
    出店者の売上をあげるために存在しているコンサルタントですが、だんだんとそれ以外の業務が増えてきてしまったので、売上アップに直接繋がらない業務は排除しました。

  • 提案の実行度が高い出店者からコンサルティングできるように改善
    まず提案するべき相手はやる気がある相手、ということで提案する店舗のリストを作成

これらを進める上でいろんな部署での経験があってよかった話

  • ここでコンサルティング業務をしたことがあってよかった話

    • 業務内容を理解している(何のために、どれくらい時間がかかるのかも)
    • 提案の実行度がわからないor低い課題を肌感を持って知っていた

    やったことないと机上の空論になってしまったり、または考えた解決策が根本的に解決しないものであったりするなと思いました。

  • ここでSQLをちょっと使えてよかった話
    コンサルティング結果の見える化をするにあたり、SQLを使っています。 どういうデータを取得したいのかを明確にして、効果測定が私だけで完結してできることがよかったです。 1日に数十件コンサルティングしていますが、例えば出店者が画像を追加した履歴とコンサルタントが接触した履歴を元に効果のある提案だったかを出しています。

今後やること

  • 担当しているリフォームカテゴリの動向をもっと追い、カテゴリ特有の問題点を把握する
  • 売上金額にするとどれくらいのインパクトがあるのか?についてはまだ関係性を出せていないので、そこは今後の課題です。
  • 分析をもっと早くできるようになるのと自動化していく

伝えたいこと

ここまで、カテゴリーマネージャーの仕事についても知ってもらいたいと思って書きました。
私たちテックチームでは「くらしのマーケット」を一緒に作る仲間を募集しています!
様々なバックグラウンドをお持ちの方、新しいことにチャレンジしてみたいという方!ぜひ一緒に働きたいです!経験を活かす方法を一緒に考えましょう! 興味がある方はぜひお気軽にコーポレートサイトからご連絡ください。

おまけ

最近はボードゲームにはまっています!
テックチームだけではなく、他の部署のメンバーも一緒にやっています。一緒にできる方もお待ちしています〜!

ではまた来週!次はへいへいさんがお届けします!

スクラムを導入してみた話

はじめに

こんにちは、ディレクターのツカモトです。会社にはネコが2匹いますが、ネコの手の色でしか違いがわからないので心の中では「シロ」「シロじゃないほう」と呼んでいます。(名前はちゃんとあります)

みんなのマーケットの一部の開発チームでは2ヶ月ほど前からスクラムを導入して開発を進めています。全員スクラム未経験からのスタートでまだまだ課題も山積みですが、良かったこともたくさんあるので今日は少し紹介したいと思います。 スクラム自体の説明は割愛しますが、「スクラムってなに?」という方はスクラムガイドを読んでみてください。 https://www.scrumguides.org/docs/scrumguide/v2017/2017-Scrum-Guide-Japanese.pdf

スクラムを導入した経緯

スクラムを導入する前の開発部門は3~4名を1チームとし、3チーム体制で基本的にメンバー固定で開発を進めていました。メンバー固定で開発をしていると、特定メンバーとのコミュニケーションは密になる反面、チーム・個人単位で知識やスキルの偏りが時間の経過とともに大きくなっていました。そしてその偏りが、開発するうえでの制約となり生産性や品質に影響を及ぼすようになってきました。また、「チームとは何なのか?」と悩んでいた時でもありました。 そんな時、「カイゼン・ジャーニー たった1人からはじめて、「越境」するチームをつくるまで」という本に出会ったこと、またこの本を他のチームのエンジニアの、のりすけが読んでくれたこともあり、わりと唐突に2チームを合体して1チームとし、PO2名エンジニア5名体制でのスクラムが始まりました。

良かったこと

f:id:curama-tech:20180518103011p:plain

経験者がひとりもいない中、本やネットでの情報を頼りに始めたスクラムですが良かったと思う点は多くありました。

  • ふりかえりの習慣ができた! -> 今は1スプリントを1週間としているので、毎週KPTでふりかえりを行なっています。スクラムを開始する前と比較するとProblemを共通認識するようになり、またProblemをチーム全体でTryに落とし込むため様々な解決案が出されるようになり解決済となったProblemは確実に増えました。
  • コミュニケーションがとても増えた! -> スクラムを始めて数週間は、多くのメンバーがふりかえりのKeepとしてコミュニケーション増加をあげていました。
  • ペアプロやモブプロが盛んに行われるようになった! -> これまでペアプロはたま〜に行われる程度でしたが、今では毎日一定の時間行われています。すごい変化!目指せ属人化排除!
  • 情報の透明化! -> タスクは今までGithubで管理していましたが、テックメンバーだけでなく全員みれるようTrelloでの管理に変更しました。タスクに関するやりとりは基本的にTrelloに集約し、「なんでxxなんだっけ?」を引き出しやすくしています。

上記に加え、エンジニアからはこんな声も寄せられました。

  • コミュニケーションが密になった。ペアプロ・モブプロをちゃんとやるようにした事によって知識が共有できたことに加えて、レビューの効率がかなり良くなった!
  • 同じ言葉で会話ができるケースが増えて、職種関わらず同じ言葉を使って話をできるようになった!
  • デプロイ前のめちゃ不安な状況をかなり軽減できてすごく嬉しい!

課題

良かった点もたくさんありますが、「スクラムは、軽量・理解が容易・習得は困難」といわれているように、習得というにはまだまだといったところです。また、当然ではありますがスクラムのデメリットにもぶつかっています。

  • 知識やスキルの偏りが大きかったためプランニングに時間がかかる&生産性が一時的に落ちている -> 一時的な話のはずなので徐々に改善すると見込んでます。今後に期待!
  • ベロシティが定まらない -> スプリントによってかなりバラつきがある。プランニングポーカーでストーリーポイントが一定数以上の場合はストーリーを分割するなどを試し中。
  • 1スプリントを1週間としているが、あっという間すぎる。Doingのまま次のスプリントを迎えてしまうことも少なくない。 -> それぞれメリットデメリットはあると思うが、近々2週間でも試してみようと思う。

さいごに

スクラムを始める前と比べてエンジニアとのコミュニケーション時間が圧倒的に増えましたが、この人達すごいなぁ〜と思うことがよくあります。そんな学びの多い人たちと一緒にプロダクトを作れることをとても嬉しく思っています。

f:id:curama-tech:20180518102921j:plain

我々みんなのマーケットテックチームでは「くらしのマーケット」を一緒に作る仲間を募集しています!どんな環境で開発しているかはこちらの記事にまとまっています。興味がある方はぜひ気軽に連絡ください (コーポレートサイト https://www.minma.jp/

次回は、めぐみちゃんがお届けします!

SVGスプライトの実装方法

はじめに

みんなのマーケットでデザイナーをしているミソサクです。

オフィスには猫が2匹います。ジャンゴ(♂)と、シー(♀)です。私のデスクにはシーちゃんがよく来ます。みんなのマーケットに入社時から2年半くらいコツコツおやつをあげたり「ケツトントン」をしてやっと友達として認められました。とっても可愛いです。だけどズルい女。

仰向けで寝るシーちゃん

先日SVGスプライトを使ったので、実装方法をざっくり書きます。

SVGスプライトとは

アイコンやロゴなど、複数のSVGデータを1つのファイルにまとめて、使いたいデータだけ呼び出す方法です。

なんで今SVGスプライト?

ずばり、スマホページの表示速度改善のためです!「写真」としての役割の画像はいいとして、「アイコン」や「あしらい」としての役割の画像について、くらしのマーケットでは形式がバラバラでした。

  1. jpg
  2. gif
  3. png
  4. svg
  5. 外部Fontファイル
  6. オリジナルFontファイル

そして20回も30回もサーバーにリクエストをしています。

最近追加したアイコンは、IcoMoonを使ってオリジナルFontファイル(上記6)1つにまとまってたんですが、ネットワークが遅いと一瞬文字化けするのが個人的に許せませんでした。ということで、最初に用意する工数は増えるけど最速でアイコンを表示できそうなSVGスプライトにしました。

達成したいこと

全ての画像(写真を除く)において

  • リクエスト数を1にする(表示速度改善)
  • cssで拡大縮小可能にする(ベクターデータにしてRetina対応)
  • cssで色指定できるようにする(色変更のために作り直したくない)

仰向けでこっちを見るシーちゃん

はい、では実装

実装

(1)SVGの準備

サイト上のすべてのアイコンを洗い出します。cssだけで表現できる画像は削除してコード化します。イラレで必要なアイコンを書いて、SVGで保存。軽量化が目的なので、書き出す時は「Illustratorの編集機能を保持」のチェックは外します。

くらしのマーケットロゴデータ

(2)SVGを一つのファイルにまとめる

まとめ役のファイルall-icons.svgの記述

<svg display="none" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <defs>
        // ここに各SVGコードを書く
    </defs>
</svg>

<defs></defs>の中身、各SVGコードの記述

<symbol id="logo" viewBox="0 0 24 24">
    <path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/>
</symbol>

お察しの通りidは呼び出す時に使います。fill属性の記述は削除してcssでの色指定を可能にします。

(3)表示する

召喚の呪文

<svg class="icon-logo">
    <title>くらしのマーケット</title>
    <desc>くらしのマーケットのロゴです。</desc>
    <use xlink:href=“/path/to/all-icons.svg#logo"/>
</svg>

#の後に呼びたいデータのid名を書きます。

(4)cssで色やサイズを指定

.icon-logo {
    fill: #fff;
    height: 24px;
    width: 24px;
}

色の指定はcolor ではなくて、fillです。

基本的な使用方法は以上ですが、アイツが黙ってるわけない。

IE11問題

IE11はSVGの表示はできるけど、use要素の表示には対応してないらしいです。svg4everybodyという便利なjsを用意してくれている英雄がいたので使わせて頂きました。

<script src="/path/to/svg4everybody.js"></script>
<script>svg4everybody();</script>

まさかのiOS 9.3問題

ここまで実装してなんとiOS 9.3で、アイコンを表示する度にall-icons.svgを毎回毎回サーバーまで呼び出している事がわかりました。キャッシュしてくれない。リクエスト数1回にするどころか、爆発的に増やしている。。。。。。。。調べても対応策は見当たらない。。。。うーん。。。。。。。。

パソコンに乗るシーちゃん
いくら可愛くても、そこは座っちゃダメ。

最終手段

そもそもSVGはコードなんだから、all-icons.svgの中身をhtmlに直接書いてしまおう! うちはテンプレートを使っているので書くのも一箇所でOK!これによって、htmlは長くなるけど、アイコン系の画像のリクエスト数は0になりました!しかもuseの記述も短くなった!

// before
<use xlink:href=“/path/to/all-icons.svg#logo"/>

// after
<use xlink:href=“#logo"/>

めでたし!めでたし!

結果

全ての画像(写真を除く)において

  • リクエスト数が0になりました!
  • cssで拡大縮小可能になりました!
  • cssで色指定できるようになりました!

以上、ざっくりですがSVGスプライトの実装方法でした!

眠るシーちゃん

最後に

我々みんなのマーケットテックチームでは「くらしのマーケット」を一緒に作る仲間を募集しています!どんな環境で開発しているかはこちらの記事にまとまっています。興味がある方はぜひ気軽に連絡ください (コーポレートサイト https://www.minma.jp/

次回は、オフィスにいる猫のオスとメスの区別がつかない、ディレクターのツカモトさんがスクラムについて書きます。