Ionicによるプライベートプロダクト開発の裏側事情

本記事は2018年4月14日に行われた[「HTML5プロダクトの裏側事情」 Ionic Meetup Nagoya #1](https://ionic-jp.connpass.com/event/82131/?utm_campaign=event_publish_to_follower&utm_medium=twitter&utm_source=notifications)の発表用資料です。

~~URLは「[https://goo.gl/2S5Cmz](https://goo.gl/2S5Cmz)」です~~
すいません。当日の資料を一旦削除して修正して再度アップしてます。

この資料について質問がありましたら[@scrpgil](https://mobile.twitter.com/scrpgil)まで連絡ください。

----

# 自己紹介
会社員とフリーランスの両輪生活をしています。
Ionic、Goを仕事でよく使います。

---


# 概要
個人的に開発しているAA(MLT)ビューアーについて技術選定や実装内容について話します。

参考リンク
[AAHub - スマホ対応のAAビューアー＆Twitter投稿サイト](https://aahub.org)

----

## 作ったサイトの紹介
AAの「まとめzip」を Web 上で閲覧できるようにしたサイトです。

![aahub.gif](https://qiita-image-store.s3.amazonaws.com/0/165335/dbbba2d9-e195-bc34-0703-9b6af630947c.gif)

主な機能
１．スマホ＆PCの両方に対応しています。
２．AAが作品別に整理されており探しやすいです。
３．AAのコピー＆ペーストや編集が行なえます。
４．画像への変換ができます（png、jpg、gif）
５．TwitterでAAをシェアできます（URL投稿型）

----

## 作った動機
スマホでやる夫スレの閲覧＆執筆をできるようにしたい思い開発を始めました。

下は最初に作っていたやる夫スレエディタです。
![output.gif](https://qiita-image-store.s3.amazonaws.com/0/165335/f2bd29ca-34ed-67ed-335c-306d2346d6e1.gif)

作り始めたら意外と難しかったので、まずはAAの扱いに慣れようと思い本ビューワを作成しているという経緯です。

### 参考リンク
https://aahub-editor.firebaseapp.com

----


## AAの良いところ
・軽いし扱いやすい
・テキスト編集はどのデバイスでもできる
・改変の敷居が低い
・過去のAAもまとめzipとして配布されている。
・種類88万以上、サイズは圧縮状態で300MB。
・カテゴリ分けされてるので機械学習用データとしても有効（かもしれない）

### 参考リンク
[KakoTemp](http://aarepo.dip.jp/KakoTemp/)

----

## その他のツール
・[AAMZ Viewer](https://aa.yaruyomi.com/)
オンラインのMLTビューアーです

・[（´д｀）Edit](https://www.gigafree.net/tool/aasupo/edit.html)、[OrinrinEditor](https://github.com/SikigamiHNQ/OrinrinEditor)、[KMBEditor](https://github.com/tar-bin/KMBEditor)
MLTビューアー＆編集機能を持つツールです。
全てWindows用。


・[DeepAA](https://qiita.com/OsciiArt/items/325714d8ab3f2b482ced)、[DeepAA on Web](https://tar-bin.github.io/DeepAAonWeb/)
画像→AAをしてくれるオープンソース＆Webサイトです。

----

# 技術選定について

----

## 全体のアーキテクチャ
![Architecture - Content Management (2).png](https://qiita-image-store.s3.amazonaws.com/0/165335/8f48a515-7b02-6989-a2dc-63143bf83758.png)
※図の作成にはCacooを使いました。

----

## Ionic
とっても簡単にアプリorPWAの作成ができるフレームワークです。
使用感はRails + Bootstrapって印象です。
iOS or Androidアプリを作成するハイブリッドフレームワークだったけど最近はPWA対応も充実してきています。
例えば、manifest.json、ServiceWorker.jsが最初から配置されていたりします。
 
### 参考リンク
[Build Amazing Native Apps and Progressive Web Apps with Ionic Framework and Angular](https://ionicframework.com/)

----

## Angular
Ionicとセットです。
2系以降でデバッグのしやすくなりました。
Web標準を意識した設計とのことなので覚えておいて損はないと思います。

### 参考リンク
[https://angular.io/](https://angular.io/)
[ep3 Angular.js | mozaic.fm](https://mozaic.fm/episodes/3/angularjs.html)
[ep24 Angular2 | mozaic.fm](https://mozaic.fm/episodes/24/angular2.html)

----

## Firebase
色々出来るBaasです。AAHubではFirebase Hosting、Firebase CloudFunctionsを利用。
Firebase Hostingは無料で独自ドメイン、https対応ができるサービス。またgit commitを伴わずデプロイ出来るので気が楽。
Cloud Functionsは月12.5万アクセスまでは無料で、AAHubではシェアページのogpタグを書き換えるために利用。

### 参考リンク
https://firebase.google.com/



----

## Google Compute Engine
Googleのクラウドサービス。殆どの作業がCLIツールのみで済むから良いです。
以下の機能が無料枠で利用可能です。
・microインスタンス
・Docker Containerのリポジトリ
・ストレージ
・NoSQLのデータベース
RDBMSは無料枠で使えませんが、もしもの時はmicroインスタンス内にWebAPIとMySQLのdockerインスタンスを立ち上げて動かすことも可能です。

### 参考リンク
[GCPの無料枠](https://cloud.google.com/free/?hl=ja)

----

## Go + Beego
APIサーバを書くのに利用。Go言語は「これがコンパイル言語かよ！」と思うくらいコンパイルが早いのが嬉しい。
BeegoはRailsみたいなフレームワーク。APIモードは自動でSwaggerドキュメントを生成してくれたりするので嬉しい。

### 参考リンク
[Beego: simple &amp; powerful Go app framework](https://beego.me/)

----

## マネタイズ
A8.netのアフィリエイトリンクをAAと共に配信しています。
バスで東京-名古屋間を移動する際に利用しています。大体、一回27円くらいの報酬です。
<img width="237" alt="Screen Shot 2018-04-07 at 20.34.29.png" src="https://qiita-image-store.s3.amazonaws.com/0/165335/dd4650d3-66a8-71e8-8266-dc40af529c2f.png">

### 参考リンク
[【アフィリエイトA8.net】日本最大級の広告数・サイト数のアフィリエイトサービス](https://www.a8.net/)

----

# 各機能の実装について

----

## レスポンシブ対応

----

### どんな機能か？
一つのHTMLファイルで複数のデバイスでの表示に対応する機能です。
<img width="827" alt="Screen Shot 2018-04-13 at 10.53.04.png" src="https://qiita-image-store.s3.amazonaws.com/0/165335/3aff0d09-a49d-28bd-f2ef-f94f23b81920.png">

----

### どうやって実装したか？
レスポンシブ対応の方法は色々ありますが、IonicだとSplit Paneを利用すると簡単に実装できます。
![aahub2.gif](https://qiita-image-store.s3.amazonaws.com/0/165335/7bc6b007-b34c-c0aa-7751-81d7613bf473.gif)

SplitPaneは、スプリットスクリーンレイアウトを実現する仕組みです。
SplitPaneを使えば左右別々に画面遷移させることも可能です。
例えばAAHubのListPageは、フォルダの用に階層を潜る場合はリスト側を画面遷移。ファイルの時はメイン側を画面遷移するようにしています。
![aahub.gif](https://qiita-image-store.s3.amazonaws.com/0/165335/dbbba2d9-e195-bc34-0703-9b6af630947c.gif)

### 参考リンク
[New Split Pane and more, Ionic 2.2.0 is out!](https://blog.ionicframework.com/ionic-2-2-0-is-out/)
[Desktop Support in Ionic](https://ionicframework.com/docs/developer-resources/desktop-support/) ※SplitPane以外の方法

----

## タブ表示の制御について

----

### どんな機能か？
各MLTファイルをタブとして開き簡単に切り替えられる機能です。やる夫スレ作者さんは大量のAAを検索＆コピペするためタブ表示ができるととてもありがたいとのことです。

![aahub4.gif](https://qiita-image-store.s3.amazonaws.com/0/165335/daf1a576-c299-2e18-6db9-1dfa8232fd87.gif)

----

### 実装上の懸念点
<img width="939" alt="Screen Shot 2018-04-11 at 22.34.59.png" src="https://qiita-image-store.s3.amazonaws.com/0/165335/3466ae83-5983-ed12-9da2-b1be6c380989.png">

タブの状態管理がとても面倒です。
上手いこと実装しないと複雑化しそう。


----

### どうやって実装したか？
今回は@ngrx/storeを利用してタブ制御を実装しました。
@ngrx/storeはAngularでReduxを実装するモジュールです。
Reduxは変更検知できるグローバル変数みたいなものです。
変更検知ができるのでStoreの値を書き換えればその値を見ている箇所は自動的に更新するようなことが簡単にできます。

<img width="716" alt="Screen Shot 2018-04-11 at 22.35.06.png" src="https://qiita-image-store.s3.amazonaws.com/0/165335/f82de694-2887-8ef0-f7db-2150b8718ebd.png">

storeの状態をlocalStorageに保持しておけばブラウザを閉じてもタブの状態を維持することができるようになりました。

### 参考リンク
[【Ionic + @ngrx 入門】 Ionic + @ngrx/store でカウンターアプリを作ってみる](https://yukiyuriweb.com/create-simple-counter-app-with-ionic-and-ngrx/)
[@ngrx/storeと@ngrx/effectsの使い方](https://qiita.com/puku0x/items/0a8e7224761dc549bd06)

----

## テキスト→画像変換

----

### どのような機能か？
アスキーアートを画像(jpg,png等)に変更する機能です。
自分で編集したアスキーアートも画像に変換できます。
<img width="292" alt="Screen Shot 2018-04-13 at 11.43.00.png" src="https://qiita-image-store.s3.amazonaws.com/0/165335/1cfc1c2b-d62e-63bf-90a1-fa9ddb91225d.png">

----

### どのように実装したか？
アスキーアートの画像化ですが、今回は2つの実装方法が候補にありました。
１．Canvasに描画したものを画像として出力
２．Go言語のライブラリggを使用する。
今回は１の方法は挫折したので2の方法で実装しています。

----

#### Canvasを使用した場合
CanvasでもfillTextを使えば文字列の描画が可能です。measureTextといったメソッドを使うことでサイズの調整もできます。Canvasに描画ができればtoDataURLを使用して画像出力が可能です。

私も最初はこの方法で実装を進めていましたが、上手くカスタムフォントのロード処理で沼にハマりました。何回やってもロード前のデフォルトフォントで描画されてしまいます。今回は上手く沼から抜け出せなかったのでサーバー側で画像化する方法に変更しています。

----

#### Go言語のライブラリggを利用する
ggは2Dグラフィックを描画するライブラリです。
文字列の描画やフォントの読み込みもできます。
シーケンスは以下。
<img width="462" alt="Screen Shot 2018-04-12 at 18.15.13.png" src="https://qiita-image-store.s3.amazonaws.com/0/165335/6a0a71ac-7025-dcce-bab2-9382fbb1e7d0.png">

画像化のためにネットワーク接続が必須になりましたが画像の最適化等クライアント側で難しい処理もできるようになりました。

### 参考リンク
[canvasでWebフォントを読み込む前にデバイスフォントが表示されてしまう問題](https://qiita.com/mo49/items/32ad489191f8e127b79c)
[Github - fogleman/gg](https://github.com/fogleman/gg)
[TinyPNG](https://tinypng.com/)

----

## 画像/テキストのダウンロード

----

### どのような機能か？
画像化したAAやMLTファイルのダウンロードが行える機能です。特筆すべき機能ではないですが、iOSが鬼門だったので紹介します。

<img width="475" alt="Screen Shot 2018-04-13 at 12.03.33.png" src="https://qiita-image-store.s3.amazonaws.com/0/165335/e4396824-b0b1-0160-9035-973a10cd4816.png">

----

### どうやって実装したか？
aタグのdownload属性を使えば簡単にダウンロードを実装できます。ただしiOSのmobile safariはdownload属性が動かないので注意が必要です。

----

#### ブラウザ、Androidの場合
サーバーから送られてきたBlobデータをwindow.URL.createObjectURL()メソッドを使いURLを作成する。
そして、aタグのhrefに設定すればよしです。簡単です。

````html
<a id="download" href="#" download="aahub_20180414.png">画像をダウンロード</a>
````

````typescript
this.marketProvider.getImage(model, mode).subscribe(data => {
    if(data){
        let d = document.getElementById("download");
        d["href"] = window.URL.createObjectURL(data);
    }
});
````

また、JavaScript上でaタグを作成→clickすることでファイルのダウンロードを行うこともできます。

````TypeScript
 var a = document.createElement('a');
 a.download = name + ".mlt";
 a.href = blobURL;
 a.click();
````

----

#### iOSの場合（その１）
iOSだとdownload属性を記述しても動作しません。
一様、aタグにtarget="_blank"をつけることで別タブとして開くことは可能です。

````html
<a id="download" href="#" download="aahub_20180414.png" target="_blank">画像をダウンロード</a>
````

<img width="815" alt="Screen Shot 2018-04-13 at 0.15.38.png" src="https://qiita-image-store.s3.amazonaws.com/0/165335/6d775065-00b5-b182-cb87-b7af8556fa9a.png">


上記が別タブで開いた状態ですがダウンロードという点ではあまり意味がないです。

しかも、この実装は以下のタグを付けた状態で「ホーム画面に追加」すると動かなくないます。

````html
<meta name="apple-mobile-web-app-capable" content="yes">
````

---

#### iOSの場合（その２）

なのでiOSでは画像ダウンロードは表示せず、iOS標準の画像長押しの保存に頼ることにしています。

<img width="320" alt="IMG_3240.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/165335/64487167-2795-cc78-8107-6ba2743f406f.png">

Ionicではこの機能がデフォルトで無効になっているのでapp.scss等に以下の記述をする必要があります。

````css
* {
    -webkit-user-select: auto;
    -khtml-user-select: auto;
    -ms-user-select: auto;
    -moz-user-select: auto;
    user-select: auto;
    -webkit-touch-callout:default;
}
````

### 参考リンク
https://caniuse.com/#search=download

----

## Twitterへの投稿機能

----

### どのような機能か？
AAを画像化してTwitterへ投稿する機能です。画像化したAAはog:imageタグにて表示させます。下の画像はTwitterに投稿した様子です。
<img width="402" alt="Screen Shot 2018-04-07 at 18.35.41.png" src="https://qiita-image-store.s3.amazonaws.com/0/165335/31481a04-3461-19c0-5d08-55c73e1fbb8b.png">

----

### どのように実装したか？
実装に向けてやるべきことは２つあります。一つはシェア用のURLの作成、もう一つはOGPタグを動的に書き換えるシェア用ページを用意することです。

----

#### シェア用URLの生成
基本的にTwitterへのURLのシェアは以下のリンクを用意してユーザーに踏ませればOKです。

````
https://twitter.com/intent/tweet?url=<シェアしたいURL>
````

シェア用URLの作成はサーバー側で実装しています。
今回、DBにユーザーの情報を持ちたくなかったので、アスキーアートの情報をそのままgzip圧縮＆Base64エンコーディングしてシェア用URLにしています。

全体的なシーケンスは以下。

<img width="540" alt="Screen Shot 2018-04-12 at 20.27.52.png" src="https://qiita-image-store.s3.amazonaws.com/0/165335/2f080d48-b3b2-ee91-99ec-f2c703897e15.png">

ちなみにクライアント側でもgzip圧縮をするライブラリーがいくつかあります。今回試したんですが、クライアント側で圧縮したデータをGo言語側で解凍できない。解答すると壊れるといった事象に見舞われました。色々回避策を試みたんですが、解決できなかったためサーバー側で実装しています。
ちなみにGo言語にはURLセーフなBase64の生成が標準ライブラリに組み込まれているのでいくらか楽に実装できます。

----

#### iOSでwindow.open()が動かない

iOSはwindow.openではなくwindow.location.hrefを使用する必要がありました。

````js
 if(window.open(url,"_blank")){
 }else{
     window.location.href = url;
 }
````

----

#### シェア用ページの用意
今回、シェア用ページはIonicとは別にCloud Function for Firebaseを利用して作成しています。Cloud Functions for Firebaseを利用するとFirebase Hostingに簡単に動的なページを追加することが可能です。firebase.jsonファイルに以下のような記述を追記します。

````js
{
  "hosting": {
    "public": "www",
    "rewrites": [ {
        "source": "/share/**", "function": "app"
    }],
//〜〜略〜〜
}
````

````javascript
import * as functions from 'firebase-functions';
import * as express from 'express';
import * as ejs from 'ejs';
const app = express();


app.set("view engine","ejs");
app.engine('ejs', ejs.__express);
app.get('/share/:path', (req, res) => {
    const path = req.params.path;
    res.render("template", { path: path});
});

exports.app = functions.https.onRequest(app);
````


<img width="377" alt="Screen Shot 2018-04-13 at 0.17.02.png" src="https://qiita-image-store.s3.amazonaws.com/0/165335/07a40ae1-9669-9ae2-93b2-1b9a17869bbe.png">

ちなみにog:imageですが、httpsで記述するとTwitterで画像が表示されるまで結構かかるようです。今回はすぐに画像を表示してほしかったのでhttpで記述しています。

----

#### あまり使われていない機能
現状、この機能はあまり使われていません。原因はURLが長すぎるのが心理的障害になっているようです。Twitterに投稿さえすれば自動的に短縮されますが、投稿画面に表示される長いURLが表示されると投稿を躊躇するみたいです。

<img width="277" alt="Screen Shot 2018-04-13 at 0.17.02.png" src="https://qiita-image-store.s3.amazonaws.com/0/165335/7ca49174-e943-e923-3bc9-e6fb54dfd2a6.png">


でもこのサービスが浸透していけば、いつか長いURLにも慣れてもらえると思うのでそれまでこの方式で突き進みたいと思います。

### 参考リンク
[Twitter Developer Documentation](https://dev.twitter.com/web/tweet-button)
[imaya/zlib.js - Github](https://github.com/imaya/zlib.js/)

----

## SEO対策

----

### どのような機能か？
URLのパス情報を元にtitleタグ、OGPタグを書き換える機能です。

### 2018/09/29追記
Googleボットに対して、動的にページ書き換えるのはスパムと認定されてしまうようです。Ionic3であれば、Googleボットはちゃんとレンダリングしてくれるようです。

----

### どのように実装したか？
シェア用ページと同じくCloud Functions for Firebaseを利用することでtitle、OGPタグの動的な書き換えが可能です。firebase.jsonファイルは以下のような感じ。

````js
{
  "hosting": {
    "public": "www",
    "rewrites": [ {
        "source": "/**", "function": "app"
    }],
//〜〜略〜〜
}
````

````js
import * as functions from 'firebase-functions';
import * as express from 'express';
import * as ejs from 'ejs';
const app = express();


app.set("view engine","ejs");
app.engine('ejs', ejs.__express);
app.get('/:url', (req, res) => {
    const url = req.params.url;
    let path = decodeURIComponent(url);
    let name = path.substring(path.lastIndexOf("/") + 1);
    res.render("path", { path: path, name: name, url:url});
});


exports.app = functions.https.onRequest(app);
````

````html
<!DOCTYPE html>
<html lang="ja" dir="ltr">
  <head>
    <meta charset="UTF-8">
    <title><%=name %> AA アスキーアート | AAHub</title>
    <meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <meta name="format-detection" content="telephone=no">
〜〜以下略〜〜
````

ちなみに、Ionicのstart時のindex.htmlだと各css、jsファイルが相対パスで記述されているのでrewritesの階層が深くなる場合はtemplateのjs、css読み込み書き換えが必要になります。

~~Search Console上でもちゃんとレンダリングされた状態で表示されます。~~
GoogleボットはちゃんとJSレンダリングしてくれるみたいです。

<img width="642" alt="Screen Shot 2018-04-13 at 21.53.17.png" src="https://qiita-image-store.s3.amazonaws.com/0/165335/7287dbcb-9adf-8727-e460-be5fbfe4d527.png">

----

### 他の方法
~~Google Bot等に対してSSRしてくれる[prerender.io](https://prerender.io/)というサービスが有るようです。
Firebase Hostingと一緒には使えないようですがNetlifyとは連携しているようです。~~
Google botに対して、表示切り替えるのは規約違反とのことでした。[prerender.io](https://prerender.io/)はGoogle Botに対しては動作せず、Twitterボット等にのみ動作するようです。


### 参考リンク
[Cloud Functions による動的コンテンツの配信](https://firebase.google.com/docs/hosting/functions?hl=ja)
[Webの未来を切り開くIonic Framework](https://speakerdeck.com/rdlabo/wep-pwa-ionic-framework) ※SEO対策とprerender.ioが紹介されています。

----

# プライベートプロダクトについて

----

## 作って感じたメリット
プライベートプロダクトは時間や技術に関する裁量は自分次第。やりたくてもやれなかったことができます。
また、プライベートプロダクトを作って得た知見は本業に役立つことも多くあります。プライベートプロダクトにて普段できないことに冒険出来る分、自分のやれることの裾の尾を広げることができるのでしょう。

----

## テーマ選定について
プライベートプロダクトはなるべく自分が興味を持っているもの。できれば問題を抱えているものをテーマとした方が良いと思います。
自分が興味のあるものなら、開発しているプロダクトが良いものかどうか自分で判断できるからです。

どうしても何も作りたいものがない人は下の本を読みとよいかもしれません。
「[小さなチーム、大きな仕事:37シグナルズ成功の法則](https://www.amazon.co.jp/%E5%B0%8F%E3%81%95%E3%81%AA%E3%83%81%E3%83%BC%E3%83%A0%E3%80%81%E5%A4%A7%E3%81%8D%E3%81%AA%E4%BB%95%E4%BA%8B%E3%80%94%E5%AE%8C%E5%85%A8%E7%89%88%E3%80%95-37%E3%82%B7%E3%82%B0%E3%83%8A%E3%83%AB%E3%82%BA%E6%88%90%E5%8A%9F%E3%81%AE%E6%B3%95%E5%89%87-%E3%82%B8%E3%82%A7%E3%82%A4%E3%82%BD%E3%83%B3%E3%83%BB%E3%83%95%E3%83%AA%E3%83%BC%E3%83%89/dp/415209267X)」
この本は、プロダクトのスモールスタートの方法が載っています。読むと何か作りたくなると思います。

----

# まとめ
今年度はPWA、GCPあたりが来そうです。なので、Ionic、GCP、Firebaseを利用して何かプライベートプロダクトを作り色々知見をためてみてはいかがでしょうか？
今日はご清聴ありがとうございました。